Merge branch 'dev'
This commit is contained in:
commit
7dd6ff12b6
51 changed files with 1642 additions and 1207 deletions
|
|
@ -14,9 +14,9 @@
|
|||
"license": "GPL-3.0-or-later",
|
||||
"author": "Equicord",
|
||||
"scripts": {
|
||||
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
|
||||
"build": "pnpm depCheck && node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
|
||||
"buildStandalone": "pnpm build --standalone",
|
||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||
"buildWeb": "pnpm depCheck && node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||
"buildWebStandalone": "pnpm buildWeb --standalone",
|
||||
"buildReporter": "pnpm buildWebStandalone --reporter --skip-extension",
|
||||
"buildReporterDesktop": "pnpm build --reporter",
|
||||
|
|
@ -36,7 +36,8 @@
|
|||
"lint:fix": "pnpm lint --fix",
|
||||
"test": "pnpm buildStandalone && pnpm testTsc && pnpm lint:fix && pnpm lint-styles && pnpm generatePluginJson",
|
||||
"testWeb": "pnpm lint:fix && pnpm buildWeb && pnpm testTsc",
|
||||
"testTsc": "tsc --noEmit"
|
||||
"testTsc": "tsc --noEmit",
|
||||
"depCheck": "node scripts/depCheck.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||
|
|
@ -96,7 +97,7 @@
|
|||
"typescript-transform-paths": "^3.5.5",
|
||||
"zip-local": "^0.3.5"
|
||||
},
|
||||
"packageManager": "pnpm@10.32.1",
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@types/less@3.0.8": "patches/@types__less@3.0.8.patch"
|
||||
|
|
|
|||
|
|
@ -79,11 +79,12 @@ export async function buildOrWatchAll(buildConfigs) {
|
|||
context(cfg).then(ctx => ctx.watch())
|
||||
));
|
||||
} else {
|
||||
await Promise.all(buildConfigs.map(cfg => build(cfg)))
|
||||
.catch(error => {
|
||||
for (const cfg of buildConfigs) {
|
||||
await build(cfg).catch(error => {
|
||||
console.error(error.message);
|
||||
process.exit(1); // exit immediately to skip the rest of the builds
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
33
scripts/depCheck.mjs
Normal file
33
scripts/depCheck.mjs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { createInterface } from "readline/promises";
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const PackageJSON = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
|
||||
|
||||
const allDeps = { ...PackageJSON.dependencies, ...PackageJSON.devDependencies };
|
||||
const requiredDeps = Object.keys(allDeps);
|
||||
const missing = requiredDeps.filter(d => {
|
||||
const v = allDeps[d];
|
||||
return !v.startsWith("link:") && !v.startsWith("workspace:") && !existsSync(join(root, "node_modules", d, "package.json"));
|
||||
});
|
||||
|
||||
if (missing.length) {
|
||||
console.error(`\x1b[31mMissing ${missing.length} package(s): ${missing.join(", ")}\x1b[0m`);
|
||||
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
||||
rl.on("SIGINT", () => { rl.close(); process.exit(1); });
|
||||
try {
|
||||
const ans = await rl.question("\x1b[33mRun 'pnpm install'? [Y/n]: \x1b[0m");
|
||||
if (!ans || /^y/i.test(ans)) execSync("pnpm install --frozen-lockfile", { cwd: root, stdio: "inherit" });
|
||||
} finally { rl.close(); }
|
||||
} else {
|
||||
console.log("\x1b[32mAll dependencies are installed.\x1b[0m");
|
||||
}
|
||||
|
|
@ -195,7 +195,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
|||
const {
|
||||
name, commands, contextMenus, managedStyle, userProfileBadges,
|
||||
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
|
||||
renderChatBarButton, chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton, messagePopoverButton,
|
||||
chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, messagePopoverButton,
|
||||
// Custom
|
||||
renderNicknameIcon, headerBarButton, audioProcessor, userAreaButton, renderProfileCollection
|
||||
} = p;
|
||||
|
|
@ -248,14 +248,10 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
|
|||
if (onMessageClick) addMessageClickListener(onMessageClick);
|
||||
|
||||
if (chatBarButton) addChatBarButton(name, chatBarButton.render, chatBarButton.icon);
|
||||
// @ts-expect-error: legacy code doesn't have icon
|
||||
else if (renderChatBarButton) addChatBarButton(name, renderChatBarButton);
|
||||
if (renderMemberListDecorator) addMemberListDecorator(name, renderMemberListDecorator);
|
||||
if (renderMessageDecoration) addMessageDecoration(name, renderMessageDecoration);
|
||||
if (renderMessageAccessory) addMessageAccessory(name, renderMessageAccessory);
|
||||
if (messagePopoverButton) addMessagePopoverButton(name, messagePopoverButton.render, messagePopoverButton.icon);
|
||||
// @ts-expect-error: legacy code doesn't have icon
|
||||
else if (renderMessagePopoverButton) addMessagePopoverButton(name, renderMessagePopoverButton);
|
||||
|
||||
// Custom
|
||||
if (renderNicknameIcon) addNicknameIcon(name, renderNicknameIcon);
|
||||
|
|
@ -277,7 +273,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
|
|||
const {
|
||||
name, commands, contextMenus, managedStyle, userProfileBadges,
|
||||
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
|
||||
renderChatBarButton, chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton, messagePopoverButton,
|
||||
chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, messagePopoverButton,
|
||||
// Custom
|
||||
renderNicknameIcon, headerBarButton, audioProcessor, userAreaButton, renderProfileCollection
|
||||
} = p;
|
||||
|
|
@ -327,11 +323,11 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
|
|||
if (onBeforeMessageSend) removeMessagePreSendListener(onBeforeMessageSend);
|
||||
if (onMessageClick) removeMessageClickListener(onMessageClick);
|
||||
|
||||
if (chatBarButton || renderChatBarButton) removeChatBarButton(name);
|
||||
if (chatBarButton) removeChatBarButton(name);
|
||||
if (renderMemberListDecorator) removeMemberListDecorator(name);
|
||||
if (renderMessageDecoration) removeMessageDecoration(name);
|
||||
if (renderMessageAccessory) removeMessageAccessory(name);
|
||||
if (messagePopoverButton || renderMessagePopoverButton) removeMessagePopoverButton(name);
|
||||
if (messagePopoverButton) removeMessagePopoverButton(name);
|
||||
|
||||
// Custom
|
||||
if (renderNicknameIcon) removeNicknameIcon(name);
|
||||
|
|
@ -355,7 +351,7 @@ export const initPluginManager = onlyOnce(function init() {
|
|||
|
||||
const pluginKeysToBind: Array<keyof PluginDef & `${"on" | "render"}${string}`> = [
|
||||
"onBeforeMessageEdit", "onBeforeMessageSend", "onMessageClick",
|
||||
"renderChatBarButton", "renderMemberListDecorator", "renderMessageAccessory", "renderMessageDecoration", "renderMessagePopoverButton",
|
||||
"renderMemberListDecorator", "renderMessageAccessory", "renderMessageDecoration",
|
||||
// Custom
|
||||
"renderNicknameIcon", "renderProfileCollection"
|
||||
];
|
||||
|
|
@ -387,11 +383,11 @@ export const initPluginManager = onlyOnce(function init() {
|
|||
|
||||
if (p.commands?.length) neededApiPlugins.add("CommandsAPI");
|
||||
if (p.onBeforeMessageEdit || p.onBeforeMessageSend || p.onMessageClick) neededApiPlugins.add("MessageEventsAPI");
|
||||
if (p.chatBarButton || p.renderChatBarButton) neededApiPlugins.add("ChatInputButtonAPI");
|
||||
if (p.chatBarButton) neededApiPlugins.add("ChatInputButtonAPI");
|
||||
if (p.renderMemberListDecorator) neededApiPlugins.add("MemberListDecoratorsAPI");
|
||||
if (p.renderMessageAccessory) neededApiPlugins.add("MessageAccessoriesAPI");
|
||||
if (p.renderMessageDecoration) neededApiPlugins.add("MessageDecorationsAPI");
|
||||
if (p.messagePopoverButton || p.renderMessagePopoverButton) neededApiPlugins.add("MessagePopoverAPI");
|
||||
if (p.messagePopoverButton) neededApiPlugins.add("MessagePopoverAPI");
|
||||
if (p.userProfileBadge) neededApiPlugins.add("BadgeAPI");
|
||||
|
||||
// Custom
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ function ProfileCollections({ props }: { props: any; }) {
|
|||
return Array.from(profileCollections)
|
||||
.map(([id, { render: Collection }]) => (
|
||||
<ErrorBoundary noop key={id} onError={e => logger.error(`Failed to render profile collection: ${id}`, e.error)}>
|
||||
{Collection({ ...props, user: props.user ?? props.currentUser })}
|
||||
<Collection {...props} user={props.user ?? props.currentUser} />
|
||||
</ErrorBoundary>
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,316 +4,291 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./style.css";
|
||||
import "./styles.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { showToast, Toasts } from "@webpack/common";
|
||||
import { ColorUtils, React, showToast, Toasts } from "@webpack/common";
|
||||
|
||||
const fileSizeLimit = 12e6;
|
||||
const cl = classNameFactory("vc-better-audio-player-");
|
||||
const CORS_PROXY = "https://cors.keiran0.workers.dev?url=";
|
||||
const MAX_FILE_SIZE = 12e6;
|
||||
|
||||
function parseFileSize(size: string) {
|
||||
const [value, unit] = size.split(" ");
|
||||
const multiplier = {
|
||||
B: 1,
|
||||
KB: 1024,
|
||||
MB: 1024 ** 2,
|
||||
GB: 1024 ** 3,
|
||||
TB: 1024 ** 4,
|
||||
}[unit];
|
||||
if (!multiplier) return;
|
||||
return parseFloat(value) * multiplier;
|
||||
interface PlayerInstance {
|
||||
mediaRef: React.RefObject<HTMLAudioElement>;
|
||||
props: { src: string; type: string; };
|
||||
}
|
||||
|
||||
function getMetadata(audioElement: HTMLElement) {
|
||||
const metadataElement = audioElement.querySelector("[class*='metadataContent']");
|
||||
const nameElement = metadataElement?.querySelector("a");
|
||||
const sizeElement = audioElement.querySelector("[class*='metadataContent'] [class*='metadataSize']");
|
||||
const url = nameElement?.getAttribute("href");
|
||||
const audioElementLink = audioElement.querySelector("audio");
|
||||
function validateColor(value: string, key: string, fallback: string) {
|
||||
if (/^\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}$/.test(value)) return;
|
||||
|
||||
if (!sizeElement?.textContent || !nameElement?.textContent || !url || !audioElementLink) return false;
|
||||
|
||||
const name = nameElement.textContent;
|
||||
const size = parseFileSize(sizeElement.textContent);
|
||||
|
||||
if (size && size > fileSizeLimit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const elements = [metadataElement?.parentElement, audioElement.querySelector("[class*='audioControls']")];
|
||||
|
||||
const computedStyle = getComputedStyle(audioElement);
|
||||
const parentBorderRadius = computedStyle.borderRadius;
|
||||
|
||||
if (settings.store.forceMoveBelow) {
|
||||
elements.forEach(element => {
|
||||
if (element) (element as HTMLElement).style.zIndex = "2";
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
size,
|
||||
url,
|
||||
audio: audioElementLink,
|
||||
parentBorderRadius: parentBorderRadius,
|
||||
};
|
||||
}
|
||||
|
||||
async function addListeners(audioElement: HTMLAudioElement, url: string, parentBorderRadius: string) {
|
||||
const madeURL = new URL(url);
|
||||
madeURL.searchParams.set("t", Date.now().toString());
|
||||
|
||||
const corsProxyUrl = "https://cors.keiran0.workers.dev?url=" + encodeURIComponent(madeURL.href);
|
||||
const response = await fetch(corsProxyUrl);
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
const audioContext = new AudioContext();
|
||||
const analyser = audioContext.createAnalyser();
|
||||
analyser.fftSize = 2048;
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
const frequencyData = new Uint8Array(bufferLength);
|
||||
|
||||
const source = audioContext.createMediaElementSource(audioElement);
|
||||
source.connect(analyser);
|
||||
analyser.connect(audioContext.destination);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const canvasContext = canvas.getContext("2d");
|
||||
if (!canvasContext) return;
|
||||
|
||||
canvas.classList.add("better-audio-visualizer");
|
||||
audioElement.parentElement?.appendChild(canvas);
|
||||
|
||||
if (parentBorderRadius) canvas.style.borderRadius = parentBorderRadius;
|
||||
|
||||
function drawVisualizer() {
|
||||
if (!audioElement.paused) {
|
||||
requestAnimationFrame(drawVisualizer);
|
||||
try {
|
||||
const rgb = ColorUtils.hexToRgb(value.replace("#", ""));
|
||||
if (rgb) {
|
||||
settings.store[key] = `${rgb.r}, ${rgb.g}, ${rgb.b}`;
|
||||
return;
|
||||
}
|
||||
} catch { /* invalid hex */ }
|
||||
|
||||
analyser.getByteTimeDomainData(dataArray);
|
||||
analyser.getByteFrequencyData(frequencyData);
|
||||
|
||||
if (!canvasContext) return;
|
||||
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (settings.store.oscilloscope) drawOscilloscope(canvasContext, canvas, dataArray, bufferLength);
|
||||
if (settings.store.spectrograph) drawSpectrograph(canvasContext, canvas, frequencyData, bufferLength);
|
||||
}
|
||||
|
||||
audioElement.src = blobUrl;
|
||||
audioElement.addEventListener("play", () => {
|
||||
if (audioContext.state === "suspended") {
|
||||
audioContext.resume();
|
||||
}
|
||||
drawVisualizer();
|
||||
});
|
||||
|
||||
audioElement.addEventListener("pause", () => {
|
||||
audioContext.suspend();
|
||||
});
|
||||
showToast(`Invalid color format for ${key}, use "R, G, B" or "#RRGGBB"`, Toasts.Type.FAILURE);
|
||||
settings.store[key] = fallback;
|
||||
}
|
||||
|
||||
function drawOscilloscope(canvasContext, canvas, dataArray, bufferLength) {
|
||||
const sliceWidth = canvas.width / bufferLength;
|
||||
function maxTypedArray(arr: Uint8Array<ArrayBufferLike>): number {
|
||||
let max = 0;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i] > max) max = arr[i];
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
function drawOscilloscope(ctx: CanvasRenderingContext2D, w: number, h: number, dataArray: Uint8Array<ArrayBufferLike>, bufferLength: number) {
|
||||
const sliceWidth = w / bufferLength;
|
||||
const [r, g, b] = settings.store.oscilloscopeColor.split(",").map(Number);
|
||||
const amp = 3;
|
||||
let x = 0;
|
||||
|
||||
const { oscilloscopeSolidColor, oscilloscopeColor } = settings.store;
|
||||
|
||||
const [r, g, b] = oscilloscopeColor.split(",").map(Number);
|
||||
|
||||
canvasContext.lineWidth = 2;
|
||||
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
|
||||
canvasContext.beginPath();
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
const v = dataArray[i] / 128.0;
|
||||
const y = (v * canvas.height) / 2;
|
||||
const v = (dataArray[i] - 128) / 128;
|
||||
const y = (h / 2) - (v * amp * h / 2);
|
||||
|
||||
if (oscilloscopeSolidColor) {
|
||||
canvasContext.strokeStyle = `rgb(${r}, ${g}, ${b})`;
|
||||
if (settings.store.oscilloscopeSolidColor) {
|
||||
ctx.strokeStyle = `rgb(${r}, ${g}, ${b})`;
|
||||
} else {
|
||||
const red = Math.min(r + (v * 100) + (i / bufferLength) * 155, 255);
|
||||
const green = Math.min(g + (v * 50) + (i / bufferLength) * 155, 255);
|
||||
const blue = Math.min(b + (v * 150) + (i / bufferLength) * 155, 255);
|
||||
|
||||
canvasContext.strokeStyle = `rgb(${red}, ${green}, ${blue})`;
|
||||
}
|
||||
|
||||
if (i === 0) {
|
||||
canvasContext.moveTo(x, y);
|
||||
} else {
|
||||
canvasContext.lineTo(x, y);
|
||||
const absV = Math.abs(v);
|
||||
ctx.strokeStyle = `rgb(${Math.min(r + absV * 100 + (i / bufferLength) * 155, 255)}, ${Math.min(g + absV * 50 + (i / bufferLength) * 155, 255)}, ${Math.min(b + absV * 150 + (i / bufferLength) * 155, 255)})`;
|
||||
}
|
||||
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
x += sliceWidth;
|
||||
}
|
||||
|
||||
canvasContext.stroke();
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawSpectrograph(canvasContext, canvas, frequencyData, bufferLength) {
|
||||
const { spectrographSolidColor, spectrographColor } = settings.store;
|
||||
const maxHeight = canvas.height;
|
||||
const barWidth = canvas.width / bufferLength;
|
||||
function drawSpectrograph(ctx: CanvasRenderingContext2D, w: number, h: number, frequencyData: Uint8Array<ArrayBufferLike>, bufferLength: number) {
|
||||
const barWidth = w / bufferLength;
|
||||
const maxVal = maxTypedArray(frequencyData);
|
||||
if (maxVal === 0) return;
|
||||
|
||||
const [r, g, b] = settings.store.spectrographColor.split(",").map(Number);
|
||||
let x = 0;
|
||||
|
||||
const maxFrequencyValue = Math.max(...frequencyData);
|
||||
|
||||
if (maxFrequencyValue === 0 || !isFinite(maxFrequencyValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
const normalizedHeight = (frequencyData[i] / maxFrequencyValue) * maxHeight;
|
||||
const barH = (frequencyData[i] / maxVal) * h;
|
||||
|
||||
if (spectrographSolidColor) {
|
||||
canvasContext.fillStyle = `rgb(${spectrographColor})`;
|
||||
if (settings.store.spectrographSolidColor) {
|
||||
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
|
||||
} else {
|
||||
const [r, g, b] = spectrographColor.split(",").map(Number);
|
||||
|
||||
const red = Math.min(r + (i / bufferLength) * 155, 255);
|
||||
const green = Math.min(g + (i / bufferLength) * 155, 255);
|
||||
const blue = Math.min(b + (i / bufferLength) * 155, 255);
|
||||
|
||||
const gradient = canvasContext.createLinearGradient(x, canvas.height - normalizedHeight, x, canvas.height);
|
||||
const gradient = ctx.createLinearGradient(x, h - barH, x, h);
|
||||
gradient.addColorStop(0, `rgb(${red}, ${green}, ${blue})`);
|
||||
|
||||
const darkerColor = `rgb(${Math.max(red - 50, 0)},${Math.max(green - 50, 0)},${Math.max(blue - 50, 0)})`;
|
||||
|
||||
gradient.addColorStop(1, darkerColor);
|
||||
canvasContext.fillStyle = gradient;
|
||||
gradient.addColorStop(1, `rgb(${Math.max(red - 50, 0)}, ${Math.max(green - 50, 0)}, ${Math.max(blue - 50, 0)})`);
|
||||
ctx.fillStyle = gradient;
|
||||
}
|
||||
|
||||
canvasContext.fillRect(x, canvas.height - normalizedHeight, barWidth, normalizedHeight);
|
||||
ctx.fillRect(x, h - barH, barWidth, barH);
|
||||
x += barWidth + 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
function scanForAudioElements(element: HTMLElement) {
|
||||
element.querySelectorAll("[class*='wrapperAudio']:not([data-better-audio-processed])").forEach(audioElement => {
|
||||
(audioElement as HTMLElement).dataset.betterAudioProcessed = "true";
|
||||
const metadata = getMetadata(audioElement as HTMLElement);
|
||||
async function fetchAudioBlob(src: string): Promise<string | null> {
|
||||
const url = new URL(src);
|
||||
url.searchParams.set("t", Date.now().toString());
|
||||
|
||||
if (!metadata) return;
|
||||
const response = await fetch(CORS_PROXY + encodeURIComponent(url.href));
|
||||
const contentLength = response.headers.get("content-length");
|
||||
if (contentLength && Number(contentLength) > MAX_FILE_SIZE) return null;
|
||||
|
||||
addListeners(metadata.audio, metadata.url, metadata.parentBorderRadius);
|
||||
});
|
||||
const blob = await response.blob();
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
function createObserver(targetNode: HTMLElement) {
|
||||
const observer = new MutationObserver(mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
if (mutation.type === "childList") {
|
||||
mutation.addedNodes.forEach(addedNode => {
|
||||
if (addedNode instanceof HTMLElement) {
|
||||
scanForAudioElements(addedNode);
|
||||
}
|
||||
});
|
||||
function Visualizer({ playerRef, src }: { playerRef: React.RefObject<HTMLAudioElement>; src: string; }) {
|
||||
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
||||
const audioCtxRef = React.useRef<AudioContext | null>(null);
|
||||
const analyserRef = React.useRef<AnalyserNode | null>(null);
|
||||
const animFrameRef = React.useRef(0);
|
||||
const setupDoneRef = React.useRef(false);
|
||||
const blobUrlRef = React.useRef<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const audio = playerRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
if (!audio || !canvas) return () => { };
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const init = async () => {
|
||||
const blobUrl = await fetchAudioBlob(src).catch(() => null);
|
||||
if (cancelled || !blobUrl) return;
|
||||
|
||||
blobUrlRef.current = blobUrl;
|
||||
|
||||
const wasPlaying = !audio.paused;
|
||||
const { currentTime } = audio;
|
||||
audio.src = blobUrl;
|
||||
audio.currentTime = currentTime;
|
||||
|
||||
const audioCtx = new AudioContext();
|
||||
const analyser = audioCtx.createAnalyser();
|
||||
analyser.fftSize = 2048;
|
||||
const source = audioCtx.createMediaElementSource(audio);
|
||||
source.connect(analyser);
|
||||
analyser.connect(audioCtx.destination);
|
||||
audioCtxRef.current = audioCtx;
|
||||
analyserRef.current = analyser;
|
||||
setupDoneRef.current = true;
|
||||
|
||||
if (wasPlaying) {
|
||||
audio.play().catch(() => { });
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(targetNode, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
return observer;
|
||||
}
|
||||
};
|
||||
|
||||
function tryHexToRgb(hex) {
|
||||
if (hex.startsWith("#")) {
|
||||
const hexMatch = hex.match(/\w\w/g);
|
||||
if (hexMatch) {
|
||||
const [r, g, b] = hexMatch.map(x => parseInt(x, 16));
|
||||
return `${r}, ${g}, ${b}`;
|
||||
}
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
const canvasCtx = canvas.getContext("2d");
|
||||
let dataArray: Uint8Array<ArrayBuffer> | null = null;
|
||||
let frequencyData: Uint8Array<ArrayBuffer> | null = null;
|
||||
|
||||
function handleColorChange(value, settingKey, defaultValue) {
|
||||
const rgbPattern = /^(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})$/;
|
||||
const draw = () => {
|
||||
const analyser = analyserRef.current;
|
||||
if (!canvasCtx || !analyser) return;
|
||||
|
||||
if (!value.match(rgbPattern)) {
|
||||
const rgb = tryHexToRgb(value);
|
||||
if (!dataArray || !frequencyData) {
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
dataArray = new Uint8Array(bufferLength);
|
||||
frequencyData = new Uint8Array(bufferLength);
|
||||
}
|
||||
|
||||
if (rgb.match(rgbPattern)) {
|
||||
settings.store[settingKey] = rgb;
|
||||
} else {
|
||||
showToast(`Invalid color format for ${settingKey}, make sure it's in the format 'R, G, B' or '#RRGGBB'`, Toasts.Type.FAILURE);
|
||||
settings.store[settingKey] = defaultValue;
|
||||
}
|
||||
} else {
|
||||
settings.store[settingKey] = value;
|
||||
}
|
||||
if (!audio.paused) animFrameRef.current = requestAnimationFrame(draw);
|
||||
|
||||
analyser.getByteTimeDomainData(dataArray);
|
||||
analyser.getByteFrequencyData(frequencyData);
|
||||
|
||||
const { width, height } = canvas.getBoundingClientRect();
|
||||
canvasCtx.clearRect(0, 0, width, height);
|
||||
if (settings.store.oscilloscope) drawOscilloscope(canvasCtx, width, height, dataArray, dataArray.length);
|
||||
if (settings.store.spectrograph) drawSpectrograph(canvasCtx, width, height, frequencyData, frequencyData.length);
|
||||
};
|
||||
|
||||
const onPlay = () => {
|
||||
if (!setupDoneRef.current) return;
|
||||
if (audioCtxRef.current?.state === "suspended") {
|
||||
audioCtxRef.current.resume();
|
||||
}
|
||||
draw();
|
||||
};
|
||||
|
||||
const onPause = () => {
|
||||
audioCtxRef.current?.suspend();
|
||||
cancelAnimationFrame(animFrameRef.current);
|
||||
};
|
||||
|
||||
audio.addEventListener("play", onPlay);
|
||||
audio.addEventListener("pause", onPause);
|
||||
init();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
audio.removeEventListener("play", onPlay);
|
||||
audio.removeEventListener("pause", onPause);
|
||||
cancelAnimationFrame(animFrameRef.current);
|
||||
audioCtxRef.current?.close();
|
||||
audioCtxRef.current = null;
|
||||
analyserRef.current = null;
|
||||
setupDoneRef.current = false;
|
||||
if (blobUrlRef.current) {
|
||||
URL.revokeObjectURL(blobUrlRef.current);
|
||||
blobUrlRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [playerRef]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return () => { };
|
||||
|
||||
const resize = () => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = rect.width * window.devicePixelRatio;
|
||||
canvas.height = rect.height * window.devicePixelRatio;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx?.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
};
|
||||
|
||||
resize();
|
||||
const observer = new ResizeObserver(resize);
|
||||
observer.observe(canvas);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
className={cl("canvas")}
|
||||
ref={canvasRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
oscilloscope: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable oscilloscope visualizer",
|
||||
description: "Enable oscilloscope visualizer.",
|
||||
default: true,
|
||||
},
|
||||
spectrograph: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable spectrograph visualizer",
|
||||
description: "Enable spectrograph visualizer.",
|
||||
default: true,
|
||||
},
|
||||
oscilloscopeSolidColor: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Use solid color for oscilloscope",
|
||||
description: "Use a solid color for the oscilloscope.",
|
||||
default: false,
|
||||
},
|
||||
oscilloscopeColor: {
|
||||
type: OptionType.STRING,
|
||||
description: "Color for oscilloscope",
|
||||
description: "Color for the oscilloscope (R, G, B or #hex).",
|
||||
default: "255, 255, 255",
|
||||
onChange: value => handleColorChange(value, "oscilloscopeColor", "255, 255, 255"),
|
||||
onChange: value => validateColor(value, "oscilloscopeColor", "255, 255, 255"),
|
||||
},
|
||||
spectrographSolidColor: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Use solid color for spectrograph",
|
||||
description: "Use a solid color for the spectrograph.",
|
||||
default: false,
|
||||
},
|
||||
spectrographColor: {
|
||||
type: OptionType.STRING,
|
||||
description: "Color for spectrograph",
|
||||
description: "Color for the spectrograph (R, G, B or #hex).",
|
||||
default: "33, 150, 243",
|
||||
onChange: value => handleColorChange(value, "spectrographColor", "33, 150, 243"),
|
||||
},
|
||||
forceMoveBelow: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Force the visualizer below the audio player",
|
||||
default: true,
|
||||
onChange: value => validateColor(value, "spectrographColor", "33, 150, 243"),
|
||||
},
|
||||
});
|
||||
|
||||
let observer: MutationObserver | null = null;
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterAudioPlayer",
|
||||
description: "Adds a spectrograph and oscilloscope visualizer to audio attachment players",
|
||||
description: "Adds a spectrograph and oscilloscope visualizer to audio attachment players.",
|
||||
authors: [EquicordDevs.creations],
|
||||
settings,
|
||||
start() {
|
||||
const waitForContent = () => {
|
||||
const targetNode = document.querySelector("[class*='content']");
|
||||
if (targetNode) {
|
||||
scanForAudioElements(targetNode as HTMLElement);
|
||||
observer = createObserver(targetNode as HTMLElement);
|
||||
} else {
|
||||
requestAnimationFrame(waitForContent);
|
||||
}
|
||||
};
|
||||
waitForContent();
|
||||
},
|
||||
stop() {
|
||||
observer?.disconnect();
|
||||
observer = null;
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "}renderPlayIcon(){",
|
||||
replacement: {
|
||||
match: /this\.renderAudio\(\):this\.renderVideo\(\)/,
|
||||
replace: "$&,$self.renderVisualizer(this)",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
renderVisualizer(player: PlayerInstance) {
|
||||
if (player.props.type !== "AUDIO") return null;
|
||||
return <Visualizer playerRef={player.mediaRef} src={player.props.src} key={player.props.src} />;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.better-audio-visualizer {
|
||||
.vc-better-audio-player-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
|
@ -7,4 +7,5 @@
|
|||
pointer-events: none;
|
||||
z-index: 1;
|
||||
border: none;
|
||||
}
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import { getGuildAcronym, getIntlMessage, getUniqueUsername } from "@utils/disco
|
|||
import { classes } from "@utils/misc";
|
||||
import { Channel, Guild, User } from "@vencord/discord-types";
|
||||
import { findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
|
||||
import { Avatar, ChannelStore, ContextMenuApi, GuildStore, PresenceStore, ReadStateStore, ThemeStore, TypingStore, useDrag, useDrop, useEffect, useRef, UserStore, useState, useStateFromStores } from "@webpack/common";
|
||||
import { ActiveJoinedThreadsStore, Avatar, ChannelStore, ContextMenuApi, GuildStore, PresenceStore, ReadStateStore, ThemeStore, TypingStore, useDrag, useDrop, useEffect, useRef, UserStore, useState, useStateFromStores } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
|
||||
import { TabContextMenu } from "./ContextMenus";
|
||||
|
|
@ -75,19 +75,29 @@ function TypingIndicator({ isTyping }: { isTyping: boolean; }) {
|
|||
: null;
|
||||
}
|
||||
|
||||
function getChannelUnreadState(channelId: string) {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
const newForumPostCount = channel?.guild_id && channel.isForumLikeChannel?.()
|
||||
? ActiveJoinedThreadsStore.getNewThreadCount(channel.guild_id, channel.id)
|
||||
: 0;
|
||||
const unreadCount = ReadStateStore.getUnreadCount(channelId) || newForumPostCount;
|
||||
|
||||
return {
|
||||
channelId,
|
||||
hasUnread: ReadStateStore.hasUnread(channelId) || newForumPostCount > 0,
|
||||
mentionCount: ReadStateStore.getMentionCount(channelId),
|
||||
unreadCount
|
||||
};
|
||||
}
|
||||
|
||||
export const NotificationDot = ({ channelIds }: { channelIds: string[]; }) => {
|
||||
const userId = UserStore.getCurrentUser()?.id;
|
||||
const { persistUnreadCountFallback } = settings.use(["persistUnreadCountFallback"]);
|
||||
const [, forceUpdate] = useState(0);
|
||||
const channelStateKey = channelIds.join(",");
|
||||
const channelStates = useStateFromStores(
|
||||
[ReadStateStore],
|
||||
() => channelIds.map(channelId => ({
|
||||
channelId,
|
||||
hasUnread: ReadStateStore.hasUnread(channelId),
|
||||
mentionCount: ReadStateStore.getMentionCount(channelId),
|
||||
unreadCount: ReadStateStore.getUnreadCount(channelId)
|
||||
}))
|
||||
[ActiveJoinedThreadsStore, ReadStateStore],
|
||||
() => channelIds.map(getChannelUnreadState)
|
||||
);
|
||||
const stateSignature = channelStates.map(state => `${state.channelId}:${Number(state.hasUnread)}:${state.mentionCount}:${state.unreadCount}`).join("|");
|
||||
const { badgeText, hasMention, shouldShow } = getNotificationDotState(
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./style.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { UserStore } from "@webpack/common";
|
||||
|
||||
const cl = classNameFactory("vc-char-counter-");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
colorEffects: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Turn on or off color effects for getting close to the character limit",
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "CharacterCounter",
|
||||
description: "Adds a character counter to the chat input",
|
||||
authors: [EquicordDevs.creations, EquicordDevs.Panniku, Devs.thororen],
|
||||
settings,
|
||||
patches: [
|
||||
{
|
||||
find: ".CREATE_FORUM_POST||",
|
||||
replacement: [
|
||||
{
|
||||
match: /(textValue:.{0,50}channelId:\i\.id\}\),)\i/,
|
||||
replace: "$1$self.renderCharCounter(arguments[0].textValue)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "#{intl::PREMIUM_MESSAGE_LENGTH_UPSELL_TOOLTIP}",
|
||||
replacement: {
|
||||
match: /return \i\?\i\(\):\i\(\)/,
|
||||
replace: "return null"
|
||||
}
|
||||
}
|
||||
],
|
||||
renderCharCounter: ErrorBoundary.wrap(text => {
|
||||
const premiumType = (UserStore.getCurrentUser()?.premiumType ?? 0);
|
||||
const charMax = premiumType === 2 ? 4000 : 2000;
|
||||
const { length } = text;
|
||||
|
||||
let color = "var(--primary-330)";
|
||||
if (settings.store.colorEffects) {
|
||||
const percentage = (length / charMax) * 100;
|
||||
if (percentage < 50) color = "var(--text-muted)";
|
||||
else if (percentage < 75) color = "var(--yellow-330)";
|
||||
else if (percentage < 90) color = "var(--orange-330)";
|
||||
else color = "var(--red-360)";
|
||||
}
|
||||
|
||||
if (!length) return null;
|
||||
return (
|
||||
<div className={cl("counter")} style={{ color }}>
|
||||
<span className={cl("count")} >{length}</span>/
|
||||
<span className={cl("max")} >{charMax}</span>
|
||||
</div>
|
||||
);
|
||||
}, { noop: true })
|
||||
});
|
||||
|
|
@ -33,11 +33,8 @@ export default definePlugin({
|
|||
Component: ComponentsTab,
|
||||
Icon: ComponentsIcon
|
||||
});
|
||||
|
||||
SettingsPlugin.settingsSectionMap.push(["EquicordDiscordComponents", "equicord_components"]);
|
||||
},
|
||||
stop() {
|
||||
removeFromArray(SettingsPlugin.customEntries, e => e.key === "equicord_components");
|
||||
removeFromArray(SettingsPlugin.settingsSectionMap, entry => entry[1] === "equicord_components");
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
// Profile banner animation effect
|
||||
find: "bannerAdjustment,noBorderRadius",
|
||||
find: "bannerAdjustment,isHovering",
|
||||
replacement: {
|
||||
match: /\i=\i=>\{(?=.{0,50}\.useReducedMotion\))/,
|
||||
replace: "$&return null;"
|
||||
|
|
|
|||
|
|
@ -166,9 +166,9 @@ export default definePlugin({
|
|||
patches: [
|
||||
// Voice user rows (voice channel sidebar list)
|
||||
{
|
||||
find: "location:\"VoiceUser\"",
|
||||
find: '"system:click_outside","user:escape"',
|
||||
replacement: {
|
||||
match: /(?<=previewIsOpen:.{0,100})"data-dnd-name":(\i)\.name,/,
|
||||
match: /(?<=GuildChannelUserContextMenu.{0,100})"data-dnd-name":(\i)\.name,/,
|
||||
replace: "$&\"data-dragify-user\":!0,\"data-user-id\":arguments[0].user?.id,draggable:!0,onDragStart:e=>$self.onUserDragStart(e,arguments[0].user),"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
284
src/equicordplugins/equibopStreamFixes.equibop/index.tsx
Normal file
284
src/equicordplugins/equibopStreamFixes.equibop/index.tsx
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { isPluginEnabled } from "@api/PluginManager";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import fakeNitro from "@plugins/fakeNitro";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { localStorage } from "@utils/localStorage";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
interface StreamQualityOpts {
|
||||
bitrateMin?: number;
|
||||
bitrateMax?: number;
|
||||
bitrateTarget?: number;
|
||||
capture?: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
framerate?: number;
|
||||
pixelCount?: number;
|
||||
};
|
||||
encode?: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
framerate?: number;
|
||||
pixelCount?: number;
|
||||
};
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
unlockQualityOptions: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Unlock stream quality options regardless of Nitro status",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
removeResolutionCap: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Allow resolutions above 720p at 60fps",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
forceEncoderSettings: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Force encoder to use configured resolution/fps",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
preventDownscale: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Prevent Discord from downscaling stream resolution",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
keyframeInterval: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Keyframe interval in ms (0 = encoder default, 5000 = every 5s)",
|
||||
default: 5000,
|
||||
restartNeeded: true,
|
||||
},
|
||||
minBitrate: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Minimum encoder bitrate in kbps",
|
||||
default: 500,
|
||||
restartNeeded: true,
|
||||
},
|
||||
raiseBitrateCaps: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Raise default desktop bitrate caps (600kbps target → 10Mbps, 3.5Mbps max → 40Mbps)",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
preventFramerateReduction: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Prevent Discord from reducing stream framerate when not speaking",
|
||||
default: true,
|
||||
restartNeeded: true,
|
||||
},
|
||||
bitsPerPixelPct: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Bits per pixel percentage for target bitrate (8 = 0.08 bpp, 12 = 0.12bpp)",
|
||||
default: 8,
|
||||
restartNeeded: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "EquibopStreamFixes",
|
||||
description: "Tries to fix stream quality on Equibop by patching Discord's encoder and quality restrictions.",
|
||||
authors: [EquicordDevs.creations],
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "this.getDefaultGoliveQuality()",
|
||||
replacement: [
|
||||
// override default stream quality with configured bitrate/resolution/fps
|
||||
{
|
||||
match: /(this\.goliveMaxQuality)=(this\.getDefaultGoliveQuality\(\))/,
|
||||
replace: "$1=$self.patchStreamQuality($2)",
|
||||
},
|
||||
// inject configured quality when stream settings change mid-stream
|
||||
{
|
||||
match: /setGoliveQuality\((\i)\)\{/,
|
||||
replace: "setGoliveQuality($1){$1=$self.patchGoliveArgs($1);",
|
||||
},
|
||||
// override encoder min/max bitrate limits
|
||||
{
|
||||
match:
|
||||
/(\i)\.encodingVideoMinBitRate=\i\.bitrateMin,\i\.encodingVideoMaxBitRate=\i\.bitrateMax/,
|
||||
replace: "$1.encodingVideoMinBitRate=$self.getMinBitrate(),$1.encodingVideoMaxBitRate=$self.getBitrateMax()",
|
||||
},
|
||||
],
|
||||
},
|
||||
// unlock stream quality options for all users regardless of nitro
|
||||
{
|
||||
find: "canUseCustomStickersEverywhere:",
|
||||
replacement: [
|
||||
{
|
||||
match: /canStreamQuality:\i,/,
|
||||
replace: "canStreamQuality:()=>true,",
|
||||
},
|
||||
{
|
||||
match: /canUseHighVideoUploadQuality:\i,/,
|
||||
replace: "canUseHighVideoUploadQuality:()=>true,",
|
||||
},
|
||||
],
|
||||
predicate: () => settings.store.unlockQualityOptions && !isPluginEnabled(fakeNitro.name),
|
||||
noWarn: true,
|
||||
},
|
||||
// remove guild premium tier restriction from stream fps options
|
||||
{
|
||||
find: "#{intl::STREAM_FPS_OPTION}",
|
||||
replacement: {
|
||||
match: /guildPremiumTier:\i\.\i\.TIER_\d,?/g,
|
||||
replace: "",
|
||||
},
|
||||
predicate: () => settings.store.unlockQualityOptions && !isPluginEnabled(fakeNitro.name),
|
||||
noWarn: true,
|
||||
},
|
||||
// allow resolutions above 720p at 60fps
|
||||
{
|
||||
find: ".RESOLUTION_720&&",
|
||||
replacement: {
|
||||
match: /\i===\i\.\i\.RESOLUTION_720&&\i!==\i\.\i\.FPS_60/,
|
||||
replace: "false",
|
||||
},
|
||||
predicate: () => settings.store.removeResolutionCap,
|
||||
},
|
||||
// force encoder width/height/fps to configured values
|
||||
{
|
||||
find: "}setDesktopEncodingOptions(",
|
||||
replacement: [
|
||||
{
|
||||
match: /setDesktopEncodingOptions\((\i),(\i),(\i)\)\{/,
|
||||
replace: "$&$1=$self.getWidth();$2=$self.getHeight();$3=$self.getFps();",
|
||||
predicate: () => settings.store.forceEncoderSettings,
|
||||
},
|
||||
// set keyframe interval for periodic quality refresh
|
||||
{
|
||||
match: /keyframeInterval=0/,
|
||||
replace: "keyframeInterval=$self.getKeyframeInterval()",
|
||||
},
|
||||
],
|
||||
},
|
||||
// raise default desktop bitrate caps at the source
|
||||
{
|
||||
find: "desktopBitrate:{",
|
||||
replacement: {
|
||||
match: /desktopBitrate:\{min:5e5,max:35e5,target:6e5\}/,
|
||||
replace: "desktopBitrate:{min:5e5,max:4e7,target:1e7}",
|
||||
},
|
||||
predicate: () => settings.store.raiseBitrateCaps,
|
||||
},
|
||||
// prevent framerate reduction when not speaking
|
||||
{
|
||||
find: "Reduced framerate after",
|
||||
replacement: {
|
||||
match: /this\.framerateReductionTimeout=setTimeout/,
|
||||
replace: "this.framerateReductionTimeout=void 0&&setTimeout",
|
||||
},
|
||||
predicate: () => settings.store.preventFramerateReduction,
|
||||
},
|
||||
],
|
||||
|
||||
getStreamConfig() {
|
||||
let quality: { frameRate?: string; resolution?: string; } | undefined;
|
||||
try {
|
||||
const state = JSON.parse(localStorage.getItem("EquibopState") ?? "{}");
|
||||
quality = state.screenshareQuality;
|
||||
} catch { }
|
||||
const framerate = Number(quality?.frameRate ?? 30);
|
||||
const height = Number(quality?.resolution ?? 720);
|
||||
const width = Math.round(height * (16 / 9));
|
||||
const pixelCount = width * height;
|
||||
|
||||
const pixelRate = pixelCount * framerate;
|
||||
const bpp = (settings.store.bitsPerPixelPct ?? 8) / 100;
|
||||
const bitrateTarget = Math.min(20000000, Math.max(1500000, Math.round(pixelRate * bpp)),);
|
||||
const bitrateMax = Math.min(40000000, Math.max(bitrateTarget * 2, 8000000));
|
||||
|
||||
return { framerate, height, width, pixelCount, bitrateTarget, bitrateMax };
|
||||
},
|
||||
|
||||
getWidth() {
|
||||
return this.getStreamConfig().width;
|
||||
},
|
||||
getHeight() {
|
||||
return this.getStreamConfig().height;
|
||||
},
|
||||
getFps() {
|
||||
return this.getStreamConfig().framerate;
|
||||
},
|
||||
getBitrateMax() {
|
||||
return this.getStreamConfig().bitrateMax;
|
||||
},
|
||||
coerceResolution(res: { height?: number; width?: number; }) {
|
||||
if (!res) return res;
|
||||
const config = this.getStreamConfig();
|
||||
return {
|
||||
...res,
|
||||
height: Math.max(res.height ?? 0, config.height),
|
||||
width: Math.max(res.width ?? 0, config.width),
|
||||
};
|
||||
},
|
||||
coerceFps(fps: number | undefined) {
|
||||
const config = this.getStreamConfig();
|
||||
return typeof fps === "number"
|
||||
? Math.max(fps, config.framerate)
|
||||
: config.framerate;
|
||||
},
|
||||
getMinBitrate() {
|
||||
return (settings.store.minBitrate ?? 500) * 1000;
|
||||
},
|
||||
getKeyframeInterval() {
|
||||
return settings.store.keyframeInterval ?? 5000;
|
||||
},
|
||||
patchGoliveArgs(opts: StreamQualityOpts) {
|
||||
const config = this.getStreamConfig();
|
||||
return {
|
||||
...opts,
|
||||
bitrateTarget: config.bitrateTarget,
|
||||
capture: {
|
||||
...opts.capture,
|
||||
width: config.width,
|
||||
height: config.height,
|
||||
framerate: config.framerate,
|
||||
},
|
||||
encode: {
|
||||
...opts.encode,
|
||||
width: config.width,
|
||||
height: config.height,
|
||||
framerate: config.framerate,
|
||||
pixelCount: config.pixelCount,
|
||||
},
|
||||
};
|
||||
},
|
||||
patchStreamQuality(opts: StreamQualityOpts) {
|
||||
const config = this.getStreamConfig();
|
||||
|
||||
Object.assign(opts, {
|
||||
bitrateMin: this.getMinBitrate(),
|
||||
bitrateMax: config.bitrateMax,
|
||||
bitrateTarget: config.bitrateTarget,
|
||||
});
|
||||
if (opts?.encode) {
|
||||
Object.assign(opts.encode, {
|
||||
framerate: config.framerate,
|
||||
width: config.width,
|
||||
height: config.height,
|
||||
pixelCount: config.pixelCount,
|
||||
});
|
||||
}
|
||||
Object.assign((opts.capture ??= {}), {
|
||||
framerate: config.framerate,
|
||||
width: config.width,
|
||||
height: config.height,
|
||||
pixelCount: config.pixelCount,
|
||||
});
|
||||
return opts;
|
||||
},
|
||||
});
|
||||
|
|
@ -6,6 +6,10 @@
|
|||
|
||||
export const CORS_PROXY = "https://cors.keiran0.workers.dev";
|
||||
|
||||
export function toProxiedUrl(url: string): string {
|
||||
return `${CORS_PROXY}?url=${encodeURIComponent(url)}`;
|
||||
export function normalizeCorsProxyUrl(url?: string): string {
|
||||
return (url?.trim() || CORS_PROXY).replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export function toProxiedUrl(url: string, corsProxyUrl = CORS_PROXY): string {
|
||||
return `${normalizeCorsProxyUrl(corsProxyUrl)}?url=${encodeURIComponent(url)}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { useForceUpdater } from "@utils/react";
|
|||
import { OptionType } from "@utils/types";
|
||||
import { React, Select, showToast, TextArea, TextInput, Toasts } from "@webpack/common";
|
||||
|
||||
import { CORS_PROXY } from "./constants";
|
||||
import { ServiceType } from "./types";
|
||||
import { parseShareXConfig } from "./utils/sharex";
|
||||
|
||||
|
|
@ -196,6 +197,12 @@ export const settings = definePluginSettings({
|
|||
default: false,
|
||||
hidden: true
|
||||
},
|
||||
corsProxyUrl: {
|
||||
type: OptionType.STRING,
|
||||
description: "CORS proxy URL used for browser uploads",
|
||||
default: CORS_PROXY,
|
||||
hidden: true
|
||||
},
|
||||
apngToGif: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Convert APNG uploads to GIF",
|
||||
|
|
@ -482,6 +489,18 @@ export function SettingsComponent() {
|
|||
/>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingTextInput
|
||||
name="CORS Proxy URL"
|
||||
description="CORS proxy used for web uploads. Leave empty to use the default proxy"
|
||||
value={store.corsProxyUrl || ""}
|
||||
onChange={v => store.corsProxyUrl = v}
|
||||
placeholder="https://your-cors-proxy.example.com"
|
||||
/>
|
||||
|
||||
<SettingsSection name="Default CORS Proxy Source" description="Source code for the default CORS proxy">
|
||||
<a href="https://codeberg.org/key/corsproxy" target="_blank" rel="noreferrer">codeberg.org/key/corsproxy</a>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection tag="label" name="Convert APNG to GIF" description="Convert APNG files to GIF format" inlineSetting>
|
||||
<Switch
|
||||
checked={store.apngToGif}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ type S3Store = {
|
|||
s3PublicUrl?: string;
|
||||
s3Prefix?: string;
|
||||
s3ForcePathStyle?: boolean;
|
||||
corsProxyUrl?: string;
|
||||
};
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
|
@ -188,7 +189,7 @@ export async function uploadToS3(
|
|||
throw new Error(result.error || "Upload failed");
|
||||
}
|
||||
} else {
|
||||
const response = await fetch(toProxiedUrl(uploadUrl.toString()), {
|
||||
const response = await fetch(toProxiedUrl(uploadUrl.toString(), settings.store.corsProxyUrl), {
|
||||
method: "PUT",
|
||||
headers: requestHeaders,
|
||||
body: fileBlob
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { CORS_PROXY, toProxiedUrl } from "@equicordplugins/fileUpload/constants";
|
||||
import { normalizeCorsProxyUrl, toProxiedUrl } from "@equicordplugins/fileUpload/constants";
|
||||
import { settings } from "@equicordplugins/fileUpload/settings";
|
||||
import { serviceLabels, ServiceType, ShareXUploaderConfig, UploadResponse } from "@equicordplugins/fileUpload/types";
|
||||
import { copyToClipboard } from "@utils/clipboard";
|
||||
|
|
@ -26,11 +26,13 @@ const Native = IS_DISCORD_DESKTOP
|
|||
const logger = new Logger("FileUpload", "#7cb7ff");
|
||||
|
||||
function toProxyUrl(url: string): string {
|
||||
if (Native || url.startsWith(`${CORS_PROXY}?url=`)) {
|
||||
const corsProxyUrl = normalizeCorsProxyUrl((settings.store as { corsProxyUrl?: string; }).corsProxyUrl);
|
||||
|
||||
if (Native || url.startsWith(`${corsProxyUrl}?url=`)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return toProxiedUrl(url);
|
||||
return toProxiedUrl(url, corsProxyUrl);
|
||||
}
|
||||
|
||||
let isUploading = false;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,11 @@
|
|||
justify-content: center;
|
||||
margin-top: 2px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--profile-body-divider-color, var(--input-border-default, var(--input-border)));
|
||||
border-top: 1px solid
|
||||
var(
|
||||
--profile-body-divider-color,
|
||||
var(--input-border-default, var(--input-border))
|
||||
);
|
||||
}
|
||||
|
||||
.vc-github-repos-button,
|
||||
|
|
@ -37,7 +41,11 @@
|
|||
.vc-github-repos-card {
|
||||
padding: 8px;
|
||||
border-radius: var(--radius-xs);
|
||||
border: 1px solid var(--profile-body-divider-color, var(--input-border-default, var(--input-border)));
|
||||
border: 1px solid
|
||||
var(
|
||||
--profile-body-divider-color,
|
||||
var(--input-border-default, var(--input-border))
|
||||
);
|
||||
}
|
||||
|
||||
.vc-github-repos-header {
|
||||
|
|
@ -227,19 +235,19 @@
|
|||
}
|
||||
|
||||
.vc-github-repos-header-repo {
|
||||
width: 20%
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.vc-github-repos-header-description {
|
||||
width: 50%
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.vc-github-repos-header-language {
|
||||
width: 15%
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.vc-github-repos-header-stars {
|
||||
width: 10%
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
/* Tab variant styles */
|
||||
|
|
@ -291,4 +299,4 @@
|
|||
.vc-github-repos-container.vc-github-repos-tab .vc-github-repos-star-icon,
|
||||
.vc-github-repos-container.vc-github-repos-tab .vc-github-repos-stars {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@
|
|||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { TypingStore, UserStore, useStateFromStores } from "@webpack/common";
|
||||
|
||||
const ThreeDots = findComponentByCodeLazy("Math.min(1,Math.max(", "dotRadius:");
|
||||
const cl = classNameFactory("vc-home-typing-");
|
||||
|
||||
const ThreeDots = findComponentByCodeLazy("Math.min(1,Math.max(", "dotRadius:");
|
||||
const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
|
||||
|
||||
export default definePlugin({
|
||||
|
|
@ -18,7 +20,7 @@ export default definePlugin({
|
|||
description: "Changes the home button to a typing indicator if someone in your dms is typing",
|
||||
authors: [Devs.Samwich],
|
||||
TypingIcon() {
|
||||
return <ThreeDots dotRadius={3} themed={true} />;
|
||||
return <ThreeDots className={cl("dots")} dotRadius={3} themed={true} />;
|
||||
},
|
||||
isTyping() {
|
||||
return useStateFromStores([TypingStore], () =>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ export default definePlugin({
|
|||
Component: () => <DataUI usersCollection={data.usersCollection} />,
|
||||
Icon: EyeIcon
|
||||
});
|
||||
SettingsPlugin.settingsSectionMap.push(["EquicordIRememberYou", "equicord_i_remember_you"]);
|
||||
|
||||
const data = (this.dataManager = await new Data().withStart());
|
||||
|
||||
|
|
@ -46,7 +45,6 @@ export default definePlugin({
|
|||
|
||||
stop() {
|
||||
removeFromArray(SettingsPlugin.customEntries, e => e.key === "equicord_i_remember_you");
|
||||
removeFromArray(SettingsPlugin.settingsSectionMap, entry => entry[1] === "equicord_i_remember_you");
|
||||
|
||||
const dataManager = this.dataManager as Data;
|
||||
removeMessagePreSendListener(dataManager._onMessagePreSend_preSend);
|
||||
|
|
|
|||
|
|
@ -35,11 +35,8 @@ export default definePlugin({
|
|||
Component: IconsTab,
|
||||
Icon: MagnifyingGlassIcon
|
||||
});
|
||||
|
||||
SettingsPlugin.settingsSectionMap.push(["EquicordDiscordIcons", "equicord_icon_viewer"]);
|
||||
},
|
||||
stop() {
|
||||
removeFromArray(SettingsPlugin.customEntries, e => e.key === "equicord_icon_viewer");
|
||||
removeFromArray(SettingsPlugin.settingsSectionMap, entry => entry[1] === "equicord_icon_viewer");
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -66,13 +66,11 @@ export default definePlugin({
|
|||
Component: openQrModal,
|
||||
Icon: QrCodeIcon
|
||||
});
|
||||
SettingsPlugin.settingsSectionMap.push(["EquicordLoginWithQR", "equicord_login_with_qr"]);
|
||||
preload();
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeFromArray(SettingsPlugin.customEntries, e => e.key === "equicord_login_with_qr");
|
||||
removeFromArray(SettingsPlugin.settingsSectionMap, entry => entry[1] === "equicord_login_with_qr");
|
||||
unload();
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@
|
|||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { disableStyle, enableStyle } from "@api/Styles";
|
||||
import { ButtonCompat } from "@components/Button";
|
||||
import { HeadingSecondary } from "@components/Heading";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { SettingsSection } from "@components/settings/tabs/plugins/components/Common";
|
||||
import { makeRange, OptionType } from "@utils/types";
|
||||
import { Button, MaskedLink, showToast, Toasts } from "@webpack/common";
|
||||
import { MaskedLink, Select, showToast, TextInput, Toasts } from "@webpack/common";
|
||||
|
||||
import hoverOnlyStyle from "./hoverOnly.css?managed";
|
||||
import { clearLyricsCache, removeTranslations } from "./spotify/lyrics/api";
|
||||
|
|
@ -36,6 +38,46 @@ function InstallInstructions() {
|
|||
);
|
||||
}
|
||||
|
||||
function LyricsProviderSettings() {
|
||||
const { store } = settings;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSection name="Lyrics Provider" description="Where lyrics are fetched from.">
|
||||
<Select
|
||||
options={[
|
||||
{ value: Provider.Lrclib, label: "LRCLIB", default: true },
|
||||
{ value: Provider.Spotify, label: "Spotify (Musixmatch)" },
|
||||
]}
|
||||
isSelected={v => v === store.lyricsProvider}
|
||||
select={v => { store.lyricsProvider = v as Provider; }}
|
||||
serialize={v => v}
|
||||
placeholder="Select a lyrics provider"
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
{store.lyricsProvider === Provider.Spotify && (
|
||||
<SettingsSection
|
||||
name="Spotify Lyrics API Base URL"
|
||||
description="Custom instance base URL (for example: http://localhost:8080)."
|
||||
>
|
||||
<TextInput
|
||||
type="text"
|
||||
value={store.spotifyLyricsApiUrl}
|
||||
onChange={v => {
|
||||
store.spotifyLyricsApiUrl = v;
|
||||
void clearLyricsCache();
|
||||
showToast("Lyrics cache purged", Toasts.Type.SUCCESS);
|
||||
}}
|
||||
placeholder="https://spotify-lyrics-api-pi.vercel.app"
|
||||
maxLength={null}
|
||||
/>
|
||||
</SettingsSection>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
hoverControls: {
|
||||
description: "Show controls on hover",
|
||||
|
|
@ -60,9 +102,24 @@ export const settings = definePluginSettings({
|
|||
description: "Where lyrics are fetched from",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ value: Provider.Spotify, label: "Spotify (Musixmatch)", default: true },
|
||||
{ value: Provider.Lrclib, label: "LRCLIB" },
|
||||
{ value: Provider.Lrclib, label: "LRCLIB", default: true },
|
||||
{ value: Provider.Spotify, label: "Spotify (Musixmatch)" },
|
||||
],
|
||||
hidden: true,
|
||||
},
|
||||
spotifyLyricsApiUrl: {
|
||||
type: OptionType.STRING,
|
||||
description: "Spotify lyrics API base URL.",
|
||||
hidden: true,
|
||||
default: "https://spotify-lyrics-api-pi.vercel.app",
|
||||
onChange: async () => {
|
||||
await clearLyricsCache();
|
||||
showToast("Lyrics cache purged", Toasts.Type.SUCCESS);
|
||||
}
|
||||
},
|
||||
lyricsProviderSettings: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: LyricsProviderSettings,
|
||||
},
|
||||
translateTo: {
|
||||
description: "Translate lyrics to - Changing this will clear existing translations",
|
||||
|
|
@ -102,15 +159,15 @@ export const settings = definePluginSettings({
|
|||
description: "Purge the lyrics cache",
|
||||
type: OptionType.COMPONENT,
|
||||
component: () => (
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
<ButtonCompat
|
||||
color={ButtonCompat.Colors.RED}
|
||||
onClick={() => {
|
||||
clearLyricsCache();
|
||||
showToast("Lyrics cache purged", Toasts.Type.SUCCESS);
|
||||
}}
|
||||
>
|
||||
Purge Cache
|
||||
</Button>
|
||||
</ButtonCompat>
|
||||
),
|
||||
},
|
||||
spotifySectionTitle: {
|
||||
|
|
@ -164,4 +221,10 @@ export const settings = definePluginSettings({
|
|||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
},
|
||||
WebsocketURL: {
|
||||
type: OptionType.STRING,
|
||||
description: "Default is ws://localhost:24123",
|
||||
default: "ws://localhost:24123",
|
||||
restartNeeded: true,
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ interface NullLyricCacheEntry {
|
|||
const nullLyricCache = new Map<string, NullLyricCacheEntry>();
|
||||
|
||||
export const lyricFetchers = {
|
||||
[Provider.Spotify]: async (track: Track) => await getLyricsSpotify(track.id),
|
||||
[Provider.Spotify]: async (track: Track) => await getLyricsSpotify(track.id, settings.store.spotifyLyricsApiUrl),
|
||||
[Provider.Lrclib]: getLyricsLrclib,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,17 @@ interface Line {
|
|||
endTimeMs: string;
|
||||
}
|
||||
|
||||
export async function getLyricsSpotify(trackId: string): Promise<LyricsData | null> {
|
||||
const resp = await fetch("https://spotify-lyrics-api-pi.vercel.app/?trackid=" + trackId);
|
||||
const defaultSpotifyLyricsApiUrl = "https://spotify-lyrics-api-pi.vercel.app";
|
||||
|
||||
function makeSpotifyLyricsApiUrl(trackId: string, customBaseUrl?: string): string {
|
||||
const normalizedBaseUrl = customBaseUrl?.trim().replace(/\/+$/, "") || defaultSpotifyLyricsApiUrl;
|
||||
const url = new URL(normalizedBaseUrl);
|
||||
url.searchParams.set("trackid", trackId);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export async function getLyricsSpotify(trackId: string, customBaseUrl?: string): Promise<LyricsData | null> {
|
||||
const resp = await fetch(makeSpotifyLyricsApiUrl(trackId, customBaseUrl));
|
||||
if (!resp.ok) return null;
|
||||
|
||||
let data: LyricsAPIResp;
|
||||
|
|
@ -30,6 +39,8 @@ export async function getLyricsSpotify(trackId: string): Promise<LyricsData | nu
|
|||
return null;
|
||||
}
|
||||
|
||||
if (data.error || !Array.isArray(data.lines) || data.lines.length < 2) return null;
|
||||
|
||||
const lyrics = data.lines;
|
||||
if (lyrics[0].startTimeMs === "0" && lyrics[lyrics.length - 1].startTimeMs === "0") return null;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import { Logger } from "@utils/Logger";
|
|||
import { proxyLazyWebpack } from "@webpack";
|
||||
import { Flux, FluxDispatcher } from "@webpack/common";
|
||||
|
||||
import { settings } from "../settings";
|
||||
|
||||
export interface Track {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -91,7 +93,7 @@ class TidalSocket {
|
|||
}
|
||||
|
||||
private initWs() {
|
||||
const url = "ws://localhost:24123";
|
||||
const url = settings.store.websocketURL || "ws://localhost:24123";
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,16 +24,7 @@ function getFreshPendingAvatar(section: PresetSection, guildId?: string): string
|
|||
? UserProfileSettingsStore.getPendingChanges?.(guildId)
|
||||
: UserProfileSettingsStore.getPendingChanges?.()) ?? {};
|
||||
const pendingObj = pending as Record<string, unknown>;
|
||||
|
||||
const candidates = [
|
||||
pendingObj.selectedAvatarRaw,
|
||||
pendingObj.presetAvatarRaw,
|
||||
pendingObj.pendingAvatar,
|
||||
pendingObj.avatar,
|
||||
pendingObj.selectedAvatarProcessed,
|
||||
pendingObj.presetAvatarProcessed
|
||||
];
|
||||
const selected = candidates.find(isImageInput);
|
||||
const selected = [pendingObj.pendingAvatar].find(isImageInput);
|
||||
if (!selected) return null;
|
||||
return typeof selected === "string" ? selected : selected.imageUri;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import { CustomStatus, DisplayNameStyles, Nameplate, ProfileEffect, ProfilePreset } from "@vencord/discord-types";
|
||||
import { AvatarDecorationData, CustomStatus, DisplayNameStyles, Nameplate, ProfileEffect, ProfilePreset } from "@vencord/discord-types";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
import { FluxDispatcher, GuildMemberStore, IconUtils, UserProfileStore, UserStore } from "@webpack/common";
|
||||
|
||||
|
|
@ -13,15 +13,9 @@ const UserProfileSettingsStore = findStoreLazy("UserProfileSettingsStore");
|
|||
const CustomStatusSettings = getUserSettingLazy("status", "customStatus")!;
|
||||
|
||||
type PendingChanges = Record<string, unknown> & {
|
||||
selectedAvatarRaw?: ImageInput;
|
||||
presetAvatarRaw?: ImageInput;
|
||||
selectedAvatarProcessed?: ImageInput;
|
||||
presetAvatarProcessed?: ImageInput;
|
||||
pendingAvatar?: ImageInput;
|
||||
avatar?: ImageInput;
|
||||
pendingBanner?: ImageInput;
|
||||
banner?: ImageInput;
|
||||
pendingAvatarDecoration?: { asset: string; skuId: string; } | null;
|
||||
pendingAvatarDecoration?: AvatarDecorationLike | null;
|
||||
pendingProfileEffect?: ProfileEffect | null;
|
||||
pendingNameplate?: Nameplate | null;
|
||||
pendingDisplayNameStyles?: DisplayNameStyles | null;
|
||||
|
|
@ -34,7 +28,11 @@ type PendingChanges = Record<string, unknown> & {
|
|||
pendingPrimaryGuildId?: string | null;
|
||||
};
|
||||
|
||||
type ImageInput = string | { imageUri: string; } | null | undefined;
|
||||
type ImageInput = string | { imageUri: string; [key: string]: unknown; } | null | undefined;
|
||||
type AvatarDecorationLike = AvatarDecorationData & {
|
||||
label?: string;
|
||||
type?: number;
|
||||
};
|
||||
type DisplayNameStylesLike = DisplayNameStyles & {
|
||||
fontId?: number;
|
||||
effectId?: number;
|
||||
|
|
@ -55,6 +53,10 @@ function dispatch(type: string, payload: Record<string, unknown>) {
|
|||
FluxDispatcher.dispatch({ type, ...payload });
|
||||
}
|
||||
|
||||
function setPendingChanges(payload: Record<string, unknown>, guildId?: string) {
|
||||
dispatch("USER_PROFILE_SETTINGS_SET_PENDING_CHANGES", guildId ? { guildId, ...payload } : payload);
|
||||
}
|
||||
|
||||
function isNonEmptyString(value: unknown): value is string {
|
||||
return typeof value === "string" && value.length > 0;
|
||||
}
|
||||
|
|
@ -65,6 +67,15 @@ function hasImageInput(value: ImageInput): boolean {
|
|||
return typeof value === "object" && isNonEmptyString(value?.imageUri);
|
||||
}
|
||||
|
||||
function hasAvatarDecoration(value: unknown): value is AvatarDecorationLike {
|
||||
return typeof value === "object"
|
||||
&& value != null
|
||||
&& "asset" in value
|
||||
&& "skuId" in value
|
||||
&& isNonEmptyString((value as { asset?: unknown; }).asset)
|
||||
&& isNonEmptyString((value as { skuId?: unknown; }).skuId);
|
||||
}
|
||||
|
||||
function normalizeDisplayNameStyles(value: DisplayNameStylesLike | null | undefined): DisplayNameStylesLike | null {
|
||||
if (!value) return null;
|
||||
const fontId = value.fontId ?? value.font_id;
|
||||
|
|
@ -140,7 +151,6 @@ export async function getCurrentProfile(guildId?: string, options: CurrentProfil
|
|||
const pendingChanges: PendingChanges = isGuildProfile && Object.keys(pendingChangesForGuild).length > 0
|
||||
? pendingChangesForGuild
|
||||
: pendingChangesDefault;
|
||||
|
||||
const customStatusSetting = CustomStatusSettings.getSetting();
|
||||
const customStatus = isGuildProfile
|
||||
? null
|
||||
|
|
@ -151,11 +161,15 @@ export async function getCurrentProfile(guildId?: string, options: CurrentProfil
|
|||
expiresAtMs: customStatusSetting?.expiresAtMs ?? "0"
|
||||
};
|
||||
|
||||
const avatarDecorationSource = pendingChanges.pendingAvatarDecoration ?? userAny.avatarDecorationData;
|
||||
const avatarDecoration = avatarDecorationSource ? {
|
||||
asset: avatarDecorationSource.asset,
|
||||
skuId: avatarDecorationSource.skuId
|
||||
} : null;
|
||||
const avatarDecorationSource = pendingChanges.pendingAvatarDecoration
|
||||
?? (isGuildProfile ? guildMember?.avatarDecoration : userAny.avatarDecorationData);
|
||||
const avatarDecoration = hasAvatarDecoration(avatarDecorationSource)
|
||||
? {
|
||||
...avatarDecorationSource,
|
||||
asset: avatarDecorationSource.asset,
|
||||
skuId: avatarDecorationSource.skuId
|
||||
}
|
||||
: null;
|
||||
|
||||
let profileEffect: ProfileEffect | null = null;
|
||||
const effectToUse = pendingChanges.pendingProfileEffect ?? userProfile?.profileEffect;
|
||||
|
|
@ -194,7 +208,8 @@ export async function getCurrentProfile(guildId?: string, options: CurrentProfil
|
|||
}
|
||||
}
|
||||
|
||||
const nameplateToUse = pendingChanges.pendingNameplate ?? userAny.collectibles?.nameplate;
|
||||
const nameplateToUse = pendingChanges.pendingNameplate
|
||||
?? (isGuildProfile ? guildMember?.collectibles?.nameplate : userAny.collectibles?.nameplate);
|
||||
const nameplate = nameplateToUse ? {
|
||||
skuId: nameplateToUse.skuId,
|
||||
asset: nameplateToUse.asset,
|
||||
|
|
@ -209,7 +224,7 @@ export async function getCurrentProfile(guildId?: string, options: CurrentProfil
|
|||
const displayNameStylesToUse = pendingChanges.pendingDisplayNameStyles ?? savedDisplayNameStyles;
|
||||
const displayNameStyles = normalizeDisplayNameStyles(displayNameStylesToUse);
|
||||
|
||||
const pendingAvatar = pendingChanges.pendingAvatar ?? pendingChanges.avatar;
|
||||
const { pendingAvatar } = pendingChanges;
|
||||
const avatarToUse: ImageInput = hasImageInput(pendingAvatar)
|
||||
? pendingAvatar
|
||||
: (isGuildProfile ? (guildMember?.avatar ?? currentUser.avatar ?? null) : (currentUser.avatar ?? null));
|
||||
|
|
@ -222,7 +237,7 @@ export async function getCurrentProfile(guildId?: string, options: CurrentProfil
|
|||
const avatarDataUrl = await processImage(avatarInput, currentUser.id, "avatar", effectiveGuildId, useGuildAvatar);
|
||||
const resolvedAvatarDataUrl = avatarDataUrl ?? IconUtils.getDefaultAvatarURL(currentUser.id);
|
||||
|
||||
const pendingBanner = pendingChanges.pendingBanner ?? pendingChanges.banner;
|
||||
const { pendingBanner } = pendingChanges;
|
||||
const bannerToUse: ImageInput = hasImageInput(pendingBanner)
|
||||
? pendingBanner
|
||||
: (isGuildProfile ? (guildProfile?.banner ?? baseProfile?.banner) : baseProfile?.banner);
|
||||
|
|
@ -239,7 +254,7 @@ export async function getCurrentProfile(guildId?: string, options: CurrentProfil
|
|||
globalName: isGuildProfile
|
||||
? (pendingChanges.pendingNickname ?? guildMember?.nick ?? null)
|
||||
: (pendingChanges.pendingGlobalName ?? currentUser.globalName ?? null),
|
||||
pronouns: isGuildProfile ? null : (pendingChanges.pendingPronouns ?? userProfile?.pronouns ?? null),
|
||||
pronouns: pendingChanges.pendingPronouns ?? userProfile?.pronouns ?? null,
|
||||
avatarDecoration,
|
||||
profileEffect,
|
||||
nameplate,
|
||||
|
|
@ -267,15 +282,7 @@ function customStatusEq(a: CustomStatus | null | undefined, b: CustomStatus | nu
|
|||
function resolvePendingAvatar(pendingChanges: PendingChanges | null): ImageInput {
|
||||
if (!pendingChanges) return null;
|
||||
|
||||
const candidates: ImageInput[] = [
|
||||
pendingChanges.selectedAvatarRaw,
|
||||
pendingChanges.presetAvatarRaw,
|
||||
pendingChanges.pendingAvatar,
|
||||
pendingChanges.avatar,
|
||||
pendingChanges.selectedAvatarProcessed,
|
||||
pendingChanges.presetAvatarProcessed
|
||||
];
|
||||
return candidates.find(hasImageInput) ?? null;
|
||||
return hasImageInput(pendingChanges.pendingAvatar) ? pendingChanges.pendingAvatar : null;
|
||||
}
|
||||
|
||||
function normalizeImageValue(value: unknown): string | null {
|
||||
|
|
@ -306,20 +313,16 @@ export async function loadPresetAsPending(preset: ProfilePreset, guildId?: strin
|
|||
try {
|
||||
const isGuild = options.isGuildProfile ?? Boolean(guildId);
|
||||
if (isGuild && !guildId) return;
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
const baseProfile = UserProfileStore.getUserProfile(currentUser.id);
|
||||
const guildProfile = isGuild && guildId ? UserProfileStore.getGuildMemberProfile(currentUser.id, guildId) : null;
|
||||
const equippedProfile = guildProfile ?? baseProfile;
|
||||
const current = await getCurrentProfile(guildId, {
|
||||
isGuildProfile: isGuild
|
||||
});
|
||||
const pendingChanges = (isGuild && guildId
|
||||
? UserProfileSettingsStore.getPendingChanges(guildId)
|
||||
: UserProfileSettingsStore.getPendingChanges());
|
||||
const setPending = (field: string, payload: Record<string, unknown>) => {
|
||||
const setPending = (payload: Record<string, unknown>) => {
|
||||
const cleanPayload = Object.fromEntries(Object.entries(payload).filter(([, v]) => v !== undefined));
|
||||
if (!Object.keys(cleanPayload).length) return;
|
||||
dispatch(`USER_PROFILE_SETTINGS_SET_PENDING_${field}`, isGuild ? { ...cleanPayload, guildId } : cleanPayload);
|
||||
setPendingChanges(cleanPayload, isGuild ? guildId : undefined);
|
||||
};
|
||||
|
||||
if ("avatarDataUrl" in preset) {
|
||||
|
|
@ -327,72 +330,73 @@ export async function loadPresetAsPending(preset: ProfilePreset, guildId?: strin
|
|||
const presetAvatar = normalizeImageValue(avatarValue);
|
||||
const currentAvatar = normalizeImageValue(current.avatarDataUrl);
|
||||
const pendingAvatar = normalizeImageValue(resolvePendingAvatar(pendingChanges));
|
||||
if (isGuild || (presetAvatar !== currentAvatar && presetAvatar !== pendingAvatar)) {
|
||||
if (presetAvatar !== currentAvatar && presetAvatar !== pendingAvatar) {
|
||||
const avatarPayload =
|
||||
typeof avatarValue === "string" && avatarValue.startsWith("data:")
|
||||
avatarValue?.startsWith?.("data:")
|
||||
? {
|
||||
assetOrigin: "NEW_ASSET",
|
||||
imageUri: avatarValue,
|
||||
description: `profilesets-${preset.name ?? "preset"}`
|
||||
}
|
||||
: avatarValue;
|
||||
setPending("AVATAR", { avatar: avatarPayload });
|
||||
setPending({ pendingAvatar: avatarPayload });
|
||||
}
|
||||
}
|
||||
|
||||
if ("bannerDataUrl" in preset && preset.bannerDataUrl !== current.bannerDataUrl) {
|
||||
setPending("BANNER", { banner: preset.bannerDataUrl });
|
||||
const bannerPayload = preset.bannerDataUrl?.startsWith?.("data:")
|
||||
? {
|
||||
assetOrigin: "NEW_ASSET",
|
||||
imageUri: preset.bannerDataUrl,
|
||||
description: `profilesets-${preset.name ?? "preset"}`
|
||||
}
|
||||
: preset.bannerDataUrl;
|
||||
setPending({ pendingBanner: bannerPayload });
|
||||
}
|
||||
|
||||
if (!options.skipBio && preset?.bio !== current?.bio) {
|
||||
const bioValue = preset.bio === null ? "" : preset.bio;
|
||||
setPending("BIO", { bio: bioValue });
|
||||
setPending({ pendingBio: preset.bio ?? "" });
|
||||
}
|
||||
|
||||
if (!isGuild && !options.skipPronouns && preset?.pronouns !== current?.pronouns) {
|
||||
const pronounsValue = preset.pronouns === null ? "" : preset.pronouns;
|
||||
setPending("PRONOUNS", { pronouns: pronounsValue });
|
||||
if (!options.skipPronouns && preset?.pronouns !== current?.pronouns) {
|
||||
setPending({ pendingPronouns: preset.pronouns ?? "" });
|
||||
}
|
||||
|
||||
if (!options.skipGlobalName && preset?.globalName !== current?.globalName) {
|
||||
if (isGuild) {
|
||||
setPending("NICKNAME", { nickname: preset.globalName });
|
||||
} else {
|
||||
setPending("GLOBAL_NAME", { globalName: preset.globalName });
|
||||
}
|
||||
setPending(isGuild ? { pendingNickname: preset.globalName } : { pendingGlobalName: preset.globalName });
|
||||
}
|
||||
|
||||
if (preset.avatarDecoration !== undefined && !avatarDecorationEq(preset.avatarDecoration, current.avatarDecoration)) {
|
||||
setPending("COLLECTIBLES_ITEM", {
|
||||
item: { type: 0, value: preset.avatarDecoration }
|
||||
setPending({
|
||||
pendingAvatarDecoration: preset.avatarDecoration
|
||||
});
|
||||
}
|
||||
|
||||
if (preset.profileEffect !== undefined && !collectibleEqBySku(preset.profileEffect, current.profileEffect)) {
|
||||
setPending("COLLECTIBLES_ITEM", {
|
||||
item: { type: 1, value: preset.profileEffect }
|
||||
setPending({
|
||||
pendingProfileEffect: preset.profileEffect
|
||||
});
|
||||
}
|
||||
|
||||
if (preset.nameplate !== undefined && !nameplateEq(preset.nameplate, current.nameplate)) {
|
||||
setPending("COLLECTIBLES_ITEM", {
|
||||
item: { type: 2, value: preset.nameplate }
|
||||
setPending({
|
||||
pendingNameplate: preset.nameplate
|
||||
});
|
||||
}
|
||||
|
||||
if (preset.displayNameStyles) {
|
||||
const presetDisplayNameStyles = normalizeDisplayNameStyles(preset.displayNameStyles);
|
||||
if (!jsonEq(presetDisplayNameStyles, current.displayNameStyles)) {
|
||||
setPending("DISPLAY_NAME_STYLES", { displayNameStyles: presetDisplayNameStyles });
|
||||
setPending({ pendingDisplayNameStyles: presetDisplayNameStyles });
|
||||
}
|
||||
}
|
||||
|
||||
if (preset.themeColors && !jsonEq(preset.themeColors, current.themeColors)) {
|
||||
setPending("THEME_COLORS", { themeColors: preset.themeColors });
|
||||
setPending({ pendingThemeColors: preset.themeColors });
|
||||
}
|
||||
|
||||
if (preset.primaryGuildId && !isGuild && preset.primaryGuildId !== current.primaryGuildId) {
|
||||
dispatch("USER_SETTINGS_SET_PENDING_PRIMARY_GUILD_ID", { primaryGuildId: preset.primaryGuildId });
|
||||
setPending({ pendingPrimaryGuildId: preset.primaryGuildId });
|
||||
}
|
||||
|
||||
if (preset.customStatus && !isGuild && !customStatusEq(preset.customStatus, current.customStatus)) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -31,12 +31,9 @@ export default definePlugin({
|
|||
Component: require("./components/ThemeTab").default,
|
||||
Icon: ColorPaletteIcon
|
||||
});
|
||||
|
||||
SettingsPlugin.settingsSectionMap.push(["EquicordThemeLibrary", "equicord_theme_library"]);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeFromArray(SettingsPlugin.customEntries, e => e.key === "equicord_theme_library");
|
||||
removeFromArray(SettingsPlugin.settingsSectionMap, entry => entry[1] === "equicord_theme_library");
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export default definePlugin({
|
|||
find: ".VOICE_PANEL}}",
|
||||
replacement: [
|
||||
{
|
||||
match: /\[\i\.\i\]:\i\}\),children:\[/,
|
||||
match: /\}\),children:\[(?=.{0,200}\.VOICE_CHANNEL_GAME_ACTIVITY_INDICATOR_VIEWED)/,
|
||||
replace: "$&$self.renderButtons(arguments[0]?.user),"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export default definePlugin({
|
|||
description: "Allows you to omit either width or height when opening an image modal",
|
||||
patches: [
|
||||
{
|
||||
// TODO: bundler compat
|
||||
find: ".renderLinkComponent",
|
||||
replacement: {
|
||||
// widthAndHeightPassed = w != null && w !== 0 && h == null || h === 0
|
||||
|
|
|
|||
|
|
@ -35,12 +35,6 @@ export default definePlugin({
|
|||
{
|
||||
match: /(?<=,NOTICE_DISMISS:function\(\i\){)return null!=(\i)/,
|
||||
replace: (m, notice) => `if(${notice}?.id=="EquicordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);${m}`
|
||||
},
|
||||
// FIXME(Bundler agressive inline): Remove the non used compability once enough time has passed
|
||||
{
|
||||
match: /(?<=function (\i)\(\i\){)return null!=(\i)(?=.+?NOTICE_DISMISS:\1)/,
|
||||
replace: (m, _, notice) => `if(${notice}?.id=="EquicordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);${m}`,
|
||||
noWarn: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,18 +110,6 @@ const settings = definePluginSettings({
|
|||
}
|
||||
});
|
||||
|
||||
const settingsSectionMap: [string, string][] = [
|
||||
["EquicordSettings", "equicord_main_panel"],
|
||||
["EquicordPlugins", "equicord_plugins_panel"],
|
||||
["EquicordThemes", "equicord_themes_panel"],
|
||||
["EquicordUpdater", "equicord_updater_panel"],
|
||||
["EquicordChangelog", "equicord_changelog_panel"],
|
||||
["EquicordCloud", "equicord_cloud_panel"],
|
||||
["EquicordBackupAndRestore", "equicord_backup_restore_panel"],
|
||||
["EquicordPatchHelper", "equicord_patch_helper_panel"],
|
||||
["EquibopSettings", "equicord_equibop_settings_panel"],
|
||||
];
|
||||
|
||||
export default definePlugin({
|
||||
name: "Settings",
|
||||
description: "Adds Settings UI and debug info",
|
||||
|
|
@ -129,7 +117,6 @@ export default definePlugin({
|
|||
required: true,
|
||||
|
||||
settings,
|
||||
settingsSectionMap,
|
||||
|
||||
patches: [
|
||||
{
|
||||
|
|
@ -189,10 +176,6 @@ export default definePlugin({
|
|||
});
|
||||
},
|
||||
|
||||
getSettingsSectionMappings() {
|
||||
return settingsSectionMap;
|
||||
},
|
||||
|
||||
buildLayout(originalLayoutBuilder: SettingsLayoutBuilder) {
|
||||
const layout = originalLayoutBuilder.buildLayout();
|
||||
if (originalLayoutBuilder.key !== "$Root") return layout;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { ChannelStore, FluxDispatcher, ReadStateStore } from "@webpack/common";
|
|||
import { ReactNode } from "react";
|
||||
|
||||
import FolderSideBar from "./FolderSideBar";
|
||||
import { areNestedRelated as areNestedRelatedInMap, getAncestorFolderIds as getAncestorFolderIdsFromMap, getChildFolderIds as getChildFolderIdsFromMap, getDescendantFolderIds as getDescendantFolderIdsFromMap, hasParentInChain as hasParentInChainInMap, sanitizeNestedFolderMap } from "./nestedFolders";
|
||||
import { areNestedRelated as areNestedRelatedInMap, getChildFolderIds as getChildFolderIdsFromMap, getDescendantFolderIds as getDescendantFolderIdsFromMap, getGuildNavigationExpandTargets, hasParentInChain as hasParentInChainInMap, sanitizeNestedFolderMap } from "./nestedFolders";
|
||||
|
||||
enum FolderIconDisplay {
|
||||
Never,
|
||||
|
|
@ -67,13 +67,27 @@ const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
|
|||
const FolderItem = findComponentByCodeLazy("FolderItem", "onExpandCollapse", "folderButtonSize");
|
||||
|
||||
const MAX_TREE_FILTER_DEPTH = 1000;
|
||||
let lastGuildId = null as string | null;
|
||||
let dispatchingFoldersClose = false;
|
||||
|
||||
function getGuildFolder(id: string) {
|
||||
return SortedGuildStore.getGuildFolders().find((folder: GuildFolder) => folder.guildIds.includes(id));
|
||||
}
|
||||
|
||||
function expandGuildNavigationPath(guildId: string | null | undefined) {
|
||||
if (!guildId || guildId === "@me") return;
|
||||
|
||||
for (const folderId of getGuildNavigationExpandTargets(
|
||||
getNestedFolderMap(),
|
||||
SortedGuildStore.getGuildFolders(),
|
||||
guildId,
|
||||
ExpandedGuildFolderStore.getExpandedFolders()
|
||||
)) {
|
||||
if (!ExpandedGuildFolderStore.isFolderExpanded(folderId)) {
|
||||
FolderUtils.toggleGuildFolderExpand(folderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeFolders() {
|
||||
for (const id of ExpandedGuildFolderStore.getExpandedFolders())
|
||||
FolderUtils.toggleGuildFolderExpand(id);
|
||||
|
|
@ -148,10 +162,6 @@ function getDescendantFolderIds(parentId: string | number): string[] {
|
|||
return getDescendantFolderIdsFromMap(getNestedFolderMap(), parentId);
|
||||
}
|
||||
|
||||
function getAncestorFolderIds(childId: string | number): string[] {
|
||||
return getAncestorFolderIdsFromMap(getNestedFolderMap(), childId);
|
||||
}
|
||||
|
||||
function nestFolder(childId: string, parentId: string) {
|
||||
if (childId === parentId) return;
|
||||
if (hasParentInChain(parentId, childId)) return;
|
||||
|
|
@ -421,8 +431,8 @@ export default definePlugin({
|
|||
replace: "canDrop:e=>$self.canDropOnFolder(e,arguments[1],arguments[3])"
|
||||
},
|
||||
{
|
||||
match: /drop\(\i\)\{(?=.{0,25}!==\i\.\i\.FOLDER)/,
|
||||
replace: "$&if($self.handleFolderDrop(arguments[0],arguments[1],arguments[2],arguments[3]))return;"
|
||||
match: /(\i),(\i)\)\{let (\i)=arguments.*?,(\i)=arguments.{0,250}drop\(\i\)\{(?=.{0,25}!==\i\.\i\.FOLDER)/,
|
||||
replace: "$&if($self.handleFolderDrop($1,$2,$3,$4))return;"
|
||||
},
|
||||
{
|
||||
match: /\[\i\.\i\.GUILD\](?=.{0,250}#{intl::DND_DROP_COMBINE})/,
|
||||
|
|
@ -440,27 +450,25 @@ export default definePlugin({
|
|||
],
|
||||
|
||||
flux: {
|
||||
GUILD_SELECT({ guildId }: { guildId: string | null; }) {
|
||||
if (guildId == null) return;
|
||||
expandGuildNavigationPath(guildId);
|
||||
},
|
||||
|
||||
CHANNEL_SELECT(data) {
|
||||
if (lastGuildId !== data.guildId) {
|
||||
lastGuildId = data.guildId;
|
||||
const guildFolder = getGuildFolder(data.guildId);
|
||||
const guildFolder = getGuildFolder(data.guildId);
|
||||
|
||||
if (guildFolder?.folderId) {
|
||||
for (const folderId of getAncestorFolderIds(guildFolder.folderId).reverse()) {
|
||||
if (!ExpandedGuildFolderStore.isFolderExpanded(folderId)) {
|
||||
FolderUtils.toggleGuildFolderExpand(folderId);
|
||||
}
|
||||
}
|
||||
if (guildFolder?.folderId) {
|
||||
expandGuildNavigationPath(data.guildId);
|
||||
|
||||
if (settings.store.forceOpen && !ExpandedGuildFolderStore.isFolderExpanded(guildFolder.folderId)) {
|
||||
FolderUtils.toggleGuildFolderExpand(guildFolder.folderId);
|
||||
}
|
||||
if (settings.store.closeServerFolder && ExpandedGuildFolderStore.isFolderExpanded(guildFolder.folderId)) {
|
||||
FolderUtils.toggleGuildFolderExpand(guildFolder.folderId);
|
||||
}
|
||||
} else if (settings.store.closeAllFolders) {
|
||||
closeFolders();
|
||||
if (settings.store.forceOpen && !ExpandedGuildFolderStore.isFolderExpanded(guildFolder.folderId)) {
|
||||
FolderUtils.toggleGuildFolderExpand(guildFolder.folderId);
|
||||
}
|
||||
if (settings.store.closeServerFolder && ExpandedGuildFolderStore.isFolderExpanded(guildFolder.folderId)) {
|
||||
FolderUtils.toggleGuildFolderExpand(guildFolder.folderId);
|
||||
}
|
||||
} else if (settings.store.closeAllFolders) {
|
||||
closeFolders();
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -644,23 +652,28 @@ export default definePlugin({
|
|||
handleFolderDrop(dragItem: FolderDragItem, targetNode: GuildTreeNode, _moveToBelow: boolean, isCombine: boolean): boolean {
|
||||
if (dragItem.type !== "folder") return false;
|
||||
|
||||
if (targetNode.type === "folder") {
|
||||
const childId = dragItem.nodeId?.toString();
|
||||
const parentId = targetNode.id?.toString();
|
||||
|
||||
if (childId == null || parentId == null) return false;
|
||||
if (childId === parentId || hasParentInChain(parentId, childId)) return false;
|
||||
|
||||
try {
|
||||
nestFolder(childId, parentId);
|
||||
FluxDispatcher.dispatch({ type: "BETTER_FOLDERS_NESTED_UPDATE" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCombine) {
|
||||
unnestFolder(dragItem.nodeId?.toString());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetNode.type !== "folder") return false;
|
||||
|
||||
const childId = dragItem.nodeId?.toString();
|
||||
const parentId = targetNode.id?.toString();
|
||||
|
||||
try {
|
||||
nestFolder(childId, parentId);
|
||||
FluxDispatcher.dispatch({ type: "BETTER_FOLDERS_NESTED_UPDATE" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
shouldShowCombineTarget(noCombine: boolean, targetNode: GuildTreeNode): boolean {
|
||||
|
|
@ -679,8 +692,7 @@ export default definePlugin({
|
|||
if (dragItem.nodeId === targetNode.id) return false;
|
||||
|
||||
if (dragItem.type === "folder" && targetNode.type === "folder") {
|
||||
if (isCombine) return !hasParentInChain(targetNode.id?.toString(), dragItem.nodeId?.toString());
|
||||
return targetNode.parentId == null;
|
||||
return !hasParentInChain(targetNode.id?.toString(), dragItem.nodeId?.toString());
|
||||
}
|
||||
|
||||
if (isCombine && dragItem.type === "folder") return false;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@
|
|||
|
||||
export type NestedFolderMap = Record<string, string>;
|
||||
|
||||
export interface GuildFolderLike {
|
||||
folderId?: string | number | null;
|
||||
guildIds?: string[];
|
||||
}
|
||||
|
||||
export function getChildFolderIds(map: NestedFolderMap, parentId: string | number): string[] {
|
||||
return Object.entries(map)
|
||||
.filter(([, id]) => id === String(parentId))
|
||||
|
|
@ -87,3 +92,27 @@ export function sanitizeNestedFolderMap(map: NestedFolderMap, validFolderIds: Se
|
|||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
export function getGuildFolderId(folders: GuildFolderLike[], guildId: string | null | undefined): string | undefined {
|
||||
if (!guildId) return;
|
||||
|
||||
return folders
|
||||
.find(folder => folder.guildIds?.includes(guildId))
|
||||
?.folderId
|
||||
?.toString();
|
||||
}
|
||||
|
||||
export function getGuildNavigationExpandTargets(
|
||||
map: NestedFolderMap,
|
||||
folders: GuildFolderLike[],
|
||||
guildId: string | null | undefined,
|
||||
expandedFolderIds: Iterable<string | number>
|
||||
): string[] {
|
||||
const folderId = getGuildFolderId(folders, guildId);
|
||||
if (!folderId) return [];
|
||||
|
||||
const expandedIds = new Set(Array.from(expandedFolderIds, id => String(id)));
|
||||
const targetFolderIds = [...getAncestorFolderIds(map, folderId).reverse(), folderId];
|
||||
|
||||
return targetFolderIds.filter(id => !expandedIds.has(id));
|
||||
}
|
||||
|
|
|
|||
7
src/plugins/characterCounter/README.md
Normal file
7
src/plugins/characterCounter/README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Character Counter
|
||||
|
||||
Adds a character counter to the message input box
|
||||
|
||||

|
||||
|
||||

|
||||
74
src/plugins/characterCounter/index.tsx
Normal file
74
src/plugins/characterCounter/index.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./style.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { UserStore } from "@webpack/common";
|
||||
|
||||
const cl = classNameFactory("vc-charCounter-");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
colorEffects: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable yellow/red colouring as you get closer to the character limit",
|
||||
default: true,
|
||||
}
|
||||
});
|
||||
|
||||
function getCounterColor(percentage: number) {
|
||||
if (!settings.store.colorEffects) return "var(--primary-330)";
|
||||
if (percentage < 50) return "var(--text-muted)";
|
||||
if (percentage < 75) return "var(--yellow-330)";
|
||||
if (percentage < 90) return "var(--orange-330)";
|
||||
return "var(--red-360)";
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "CharacterCounter",
|
||||
description: "Adds a character counter to the chat input",
|
||||
authors: [Devs.thororen],
|
||||
settings,
|
||||
patches: [
|
||||
{
|
||||
find: ".CREATE_FORUM_POST||",
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=textValue:(\i),editorHeight:\i,channelId:\i\.id\}\)),\i/,
|
||||
replace: ",$self.renderCharCounter({text:$1})"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "#{intl::PREMIUM_MESSAGE_LENGTH_UPSELL_TOOLTIP}",
|
||||
replacement: {
|
||||
match: /return \i\?\i\(\):\i\(\)(?<=#{intl::PREMIUM_MESSAGE_LENGTH_UPSELL_TOOLTIP_WITHOUT_LINK}.{0,200}?)/,
|
||||
replace: "return null"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
renderCharCounter: ErrorBoundary.wrap(({ text }: { text: string; }) => {
|
||||
if (!text.length) return null;
|
||||
|
||||
const premiumType = UserStore.getCurrentUser().premiumType ?? 0;
|
||||
const charMax = premiumType === 2 ? 4000 : 2000;
|
||||
|
||||
const color = getCounterColor((text.length / charMax) * 100);
|
||||
|
||||
return (
|
||||
<div className={cl("counter")} style={{ color }}>
|
||||
<span className={cl("count")}>{text.length}</span>
|
||||
/
|
||||
<span className={cl("max")}>{charMax}</span>
|
||||
</div>
|
||||
);
|
||||
}, { noop: true })
|
||||
});
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
.vc-char-counter-counter {
|
||||
.vc-charCounter-counter {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 6px;
|
||||
right: 11px;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
background: transparent;
|
||||
transition: color 0.3s ease;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.vc-char-counter-counter .vc-char-counter-count {
|
||||
.vc-charCounter-count {
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
|
@ -92,8 +92,7 @@ export default definePlugin({
|
|||
find: "Clear Treatment ",
|
||||
replacement: [
|
||||
{
|
||||
// TODO: stable compat optional chaining remove once some time has passed
|
||||
match: /\i\??\.isStaff\(\)/,
|
||||
match: /\i\?\.isStaff\(\)/,
|
||||
replace: "true"
|
||||
},
|
||||
// Fix some tricky experiments name causing a client crash
|
||||
|
|
|
|||
|
|
@ -7,50 +7,146 @@
|
|||
import "./styles.css";
|
||||
|
||||
import { Heading } from "@components/Heading";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import type { IPluginOptionComponentProps } from "@utils/types";
|
||||
import type { Channel } from "@vencord/discord-types";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { TextInput, useState } from "@webpack/common";
|
||||
import { ChannelStore, IconUtils, Popout, SelectedChannelStore, TextInput, useRef, useState, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { settings } from ".";
|
||||
|
||||
type EmojiData = {
|
||||
id?: string | null;
|
||||
name: string;
|
||||
};
|
||||
export const MAX_ADDITIONAL_REACT_EMOJIS = 8;
|
||||
const cl = classNameFactory("vc-message-click-actions-");
|
||||
|
||||
type EmojiSelectPayload = {
|
||||
id: string | null;
|
||||
name: string;
|
||||
id?: string | null;
|
||||
name?: string | null;
|
||||
optionallyDiverseSequence?: string;
|
||||
animated?: boolean;
|
||||
};
|
||||
|
||||
const StatusEmojiPicker = findComponentByCodeLazy("setCustomStatusEmoji", "setIsEmojiPickerOpen", "selectedDefaultStatus");
|
||||
export const MAX_ADDITIONAL_REACT_EMOJIS = 8;
|
||||
type ReactionEmojiPickerProps = {
|
||||
channel?: Channel | null;
|
||||
closePopout(): void;
|
||||
onSelectEmoji(selection: {
|
||||
emoji: EmojiSelectPayload | null;
|
||||
willClose: boolean;
|
||||
}): void;
|
||||
};
|
||||
|
||||
function getEmojiValue(emoji: EmojiData) {
|
||||
return emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name;
|
||||
const ReactionEmojiPicker = findComponentByCodeLazy<ReactionEmojiPickerProps>(
|
||||
"showAddEmojiButton:",
|
||||
"pickerIntention:",
|
||||
"messageId:"
|
||||
);
|
||||
|
||||
function parseCustomEmoji(value: string) {
|
||||
return value.match(/^(?:<(?:(a):)?|:)?([\w-]+?)(?:~\d+)?:([0-9]+)>?$/);
|
||||
}
|
||||
|
||||
function parseEmojiValue(value: string): EmojiSelectPayload | null {
|
||||
function getEmojiValue(emoji: EmojiSelectPayload | null | undefined) {
|
||||
if (!emoji) return "";
|
||||
if (emoji.id && emoji.name) return `${emoji.name}:${emoji.id}`;
|
||||
if (emoji.optionallyDiverseSequence?.trim()) return emoji.optionallyDiverseSequence;
|
||||
return emoji.name?.trim() ?? "";
|
||||
}
|
||||
|
||||
function toRenderedEmoji(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const customEmoji = trimmed.match(/^:?([\w-]+):(\d+)$/);
|
||||
const customEmoji = parseCustomEmoji(trimmed);
|
||||
if (customEmoji) {
|
||||
return {
|
||||
id: customEmoji[2],
|
||||
name: customEmoji[1],
|
||||
animated: false
|
||||
kind: "custom" as const,
|
||||
id: customEmoji[3],
|
||||
name: customEmoji[2],
|
||||
animated: customEmoji[1] === "a"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: null,
|
||||
kind: "unicode" as const,
|
||||
name: trimmed,
|
||||
animated: false
|
||||
};
|
||||
}
|
||||
|
||||
function getCustomEmojiSources(id: string, animated: boolean) {
|
||||
const staticUrl = IconUtils.getEmojiURL({ id, animated: false, size: 48 });
|
||||
if (!animated) return [staticUrl];
|
||||
|
||||
const animatedUrl = IconUtils.getEmojiURL({ id, animated: true, size: 48 });
|
||||
return animatedUrl === staticUrl ? [staticUrl] : [animatedUrl, staticUrl];
|
||||
}
|
||||
|
||||
function CustomEmojiPreview({
|
||||
id,
|
||||
name,
|
||||
animated
|
||||
}: {
|
||||
id: string;
|
||||
name: string;
|
||||
animated: boolean;
|
||||
}) {
|
||||
const sources = getCustomEmojiSources(id, animated);
|
||||
const [srcIndex, setSrcIndex] = useState(0);
|
||||
|
||||
return (
|
||||
<img
|
||||
src={sources[srcIndex]}
|
||||
alt={name}
|
||||
width={34}
|
||||
height={34}
|
||||
className={cl("emoji-preview")}
|
||||
onError={() => setSrcIndex(current => current < sources.length - 1 ? current + 1 : current)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EmojiPickerButton({
|
||||
onSelect,
|
||||
children
|
||||
}: {
|
||||
onSelect(value: string): void;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const channel = useStateFromStores([SelectedChannelStore, ChannelStore], () => {
|
||||
const channelId = SelectedChannelStore.getChannelId();
|
||||
return channelId ? ChannelStore.getChannel(channelId) : null;
|
||||
});
|
||||
|
||||
return (
|
||||
<Popout
|
||||
position="bottom"
|
||||
align="left"
|
||||
targetElementRef={triggerRef}
|
||||
renderPopout={({ closePopout }) => (
|
||||
<ReactionEmojiPicker
|
||||
channel={channel}
|
||||
closePopout={closePopout}
|
||||
onSelectEmoji={({ emoji, willClose }) => {
|
||||
const nextValue = getEmojiValue(emoji);
|
||||
if (nextValue) onSelect(nextValue);
|
||||
if (willClose) closePopout();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{popoutProps => (
|
||||
<div
|
||||
{...popoutProps}
|
||||
ref={triggerRef}
|
||||
className={cl("emoji-trigger")}
|
||||
>
|
||||
{children ?? "Pick Emoji"}
|
||||
</div>
|
||||
)}
|
||||
</Popout>
|
||||
);
|
||||
}
|
||||
|
||||
function parseEmojiList(value: string) {
|
||||
return Array.from(new Set(
|
||||
value
|
||||
|
|
@ -60,37 +156,47 @@ function parseEmojiList(value: string) {
|
|||
)).slice(0, MAX_ADDITIONAL_REACT_EMOJIS);
|
||||
}
|
||||
|
||||
function addEmojiToList(list: string, emoji: string) {
|
||||
const parsedList = parseEmojiList(list);
|
||||
if (parsedList.includes(emoji)) return parsedList.join(", ");
|
||||
|
||||
return [...parsedList, emoji]
|
||||
.slice(0, MAX_ADDITIONAL_REACT_EMOJIS)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function EmojiPreview({ value }: { value: string; }) {
|
||||
const renderedEmoji = toRenderedEmoji(value);
|
||||
if (!renderedEmoji) return <>Pick Emoji</>;
|
||||
|
||||
if (renderedEmoji.kind === "custom") {
|
||||
return (
|
||||
<CustomEmojiPreview
|
||||
id={renderedEmoji.id}
|
||||
name={renderedEmoji.name}
|
||||
animated={renderedEmoji.animated}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <span className={cl("unicode-preview")}>{renderedEmoji.name}</span>;
|
||||
}
|
||||
|
||||
export function ReactEmojiSetting({ setValue }: IPluginOptionComponentProps) {
|
||||
const [emoji, setEmoji] = useState(settings.store.reactEmoji ?? "💀");
|
||||
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Heading>Select Emoji For Reactions</Heading>
|
||||
<div
|
||||
className="vc-message-click-actions-primary-emoji-picker"
|
||||
onClick={event => event.stopPropagation()}
|
||||
onMouseDown={event => event.stopPropagation()}
|
||||
>
|
||||
<StatusEmojiPicker
|
||||
customStatusEmoji={parseEmojiValue(emoji)}
|
||||
setCustomStatusEmoji={(selectedEmoji: EmojiSelectPayload | null) => {
|
||||
if (!selectedEmoji) {
|
||||
setEmoji("");
|
||||
setValue("");
|
||||
setIsEmojiPickerOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const value = getEmojiValue(selectedEmoji);
|
||||
setEmoji(value);
|
||||
setValue(value);
|
||||
setIsEmojiPickerOpen(false);
|
||||
<div className={cl("primary-emoji-picker")}>
|
||||
<EmojiPickerButton
|
||||
onSelect={newValue => {
|
||||
setEmoji(newValue);
|
||||
setValue(newValue);
|
||||
}}
|
||||
selectedDefaultStatus={isEmojiPickerOpen ? null : "online"}
|
||||
setIsEmojiPickerOpen={setIsEmojiPickerOpen}
|
||||
defaultStatusVariant="small"
|
||||
/>
|
||||
>
|
||||
<EmojiPreview value={emoji} />
|
||||
</EmojiPickerButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -99,8 +205,6 @@ export function ReactEmojiSetting({ setValue }: IPluginOptionComponentProps) {
|
|||
export function AdditionalReactEmojisSetting({ setValue }: IPluginOptionComponentProps) {
|
||||
const { addAdditionalReacts } = settings.use(["addAdditionalReacts"]);
|
||||
const [emojiList, setEmojiList] = useState(settings.store.additionalReactEmojis ?? "");
|
||||
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
|
||||
const [pickerEmoji, setPickerEmoji] = useState<EmojiSelectPayload | null>(parseEmojiValue("😀"));
|
||||
|
||||
if (!addAdditionalReacts) return null;
|
||||
|
||||
|
|
@ -122,8 +226,6 @@ export function AdditionalReactEmojisSetting({ setValue }: IPluginOptionComponen
|
|||
onChange={newValue => {
|
||||
setEmojiList(newValue);
|
||||
setValue(newValue);
|
||||
const [first] = parseEmojiList(newValue);
|
||||
setPickerEmoji(parseEmojiValue(first ?? "😀"));
|
||||
}}
|
||||
onClick={event => event.stopPropagation()}
|
||||
onMouseDown={event => event.stopPropagation()}
|
||||
|
|
@ -131,39 +233,13 @@ export function AdditionalReactEmojisSetting({ setValue }: IPluginOptionComponen
|
|||
flex: 1
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
onClick={event => event.stopPropagation()}
|
||||
onMouseDown={event => event.stopPropagation()}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
transform: "scale(1.2)",
|
||||
transformOrigin: "center"
|
||||
<EmojiPickerButton
|
||||
onSelect={newValue => {
|
||||
const nextValue = addEmojiToList(emojiList, newValue);
|
||||
setEmojiList(nextValue);
|
||||
setValue(nextValue);
|
||||
}}
|
||||
>
|
||||
<StatusEmojiPicker
|
||||
customStatusEmoji={pickerEmoji}
|
||||
setCustomStatusEmoji={(selectedEmoji: EmojiSelectPayload | null) => {
|
||||
if (!selectedEmoji) {
|
||||
setIsEmojiPickerOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const value = getEmojiValue(selectedEmoji);
|
||||
const parsed = parseEmojiList(emojiList);
|
||||
const merged = parsed.includes(value)
|
||||
? parsed
|
||||
: [...parsed, value].slice(0, MAX_ADDITIONAL_REACT_EMOJIS);
|
||||
const nextValue = merged.join(", ");
|
||||
setEmojiList(nextValue);
|
||||
setValue(nextValue);
|
||||
setPickerEmoji(selectedEmoji);
|
||||
setIsEmojiPickerOpen(false);
|
||||
}}
|
||||
selectedDefaultStatus={isEmojiPickerOpen ? null : "online"}
|
||||
setIsEmojiPickerOpen={setIsEmojiPickerOpen}
|
||||
defaultStatusVariant="small"
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -287,9 +287,9 @@ function normalizeEmoji(emoji: string): string | null {
|
|||
const trimmed = emoji.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const customMatch = trimmed.match(/^:?([\w-]+):(\d+)$/);
|
||||
const customMatch = trimmed.match(/^(?:<(?:(a):)?|:)?([\w-]+?)(?:~\d+)?:([0-9]+)>?$/);
|
||||
if (customMatch) {
|
||||
return `${customMatch[1]}:${customMatch[2]}`;
|
||||
return `${customMatch[2]}:${customMatch[3]}`;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,22 @@
|
|||
.vc-message-click-actions-primary-emoji-picker {
|
||||
margin-top: 8px;
|
||||
margin-left: 6px;
|
||||
.vc-message-click-actions-emoji-trigger {
|
||||
min-width: 46px;
|
||||
height: 46px;
|
||||
padding: 0 8px;
|
||||
display: inline-flex;
|
||||
transform: scale(1.3);
|
||||
transform-origin: top left;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: var(--background-secondary-alt);
|
||||
border: 1px solid var(--input-border);
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.vc-message-click-actions-emoji-preview {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.vc-message-click-actions-unicode-preview {
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { BaseText } from "@components/BaseText";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { FormSwitch } from "@components/FormSwitch";
|
||||
import { TooltipContainer } from "@components/TooltipContainer";
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { Margins } from "@utils/margins";
|
||||
|
|
@ -14,7 +15,7 @@ import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, Mod
|
|||
import { findCssClassesLazy } from "@webpack";
|
||||
import { TabBar, Timestamp, useState } from "@webpack/common";
|
||||
|
||||
import { parseEditContent } from ".";
|
||||
import { parseEditContent, settings } from ".";
|
||||
|
||||
const CodeContainerClasses = findCssClassesLazy("markup", "codeContainer");
|
||||
const MiscClasses = findCssClassesLazy("messageContent", "markupRtl");
|
||||
|
|
@ -34,6 +35,7 @@ export function openHistoryModal(message: any) {
|
|||
|
||||
export function HistoryModal({ modalProps, message }: { modalProps: ModalProps; message: any; }) {
|
||||
const [currentTab, setCurrentTab] = useState(message.editHistory.length);
|
||||
const [showDiff, setShowDiff] = useState(settings.store.showEditDiffs);
|
||||
const timestamps = [message.firstEditTimestamp, ...message.editHistory.map(m => m.timestamp)];
|
||||
const contents = [...message.editHistory.map(m => m.content), message.content];
|
||||
|
||||
|
|
@ -45,6 +47,7 @@ export function HistoryModal({ modalProps, message }: { modalProps: ModalProps;
|
|||
</ModalHeader>
|
||||
|
||||
<ModalContent className={cl("contents")}>
|
||||
<FormSwitch title="Show Diff" value={showDiff} onChange={setShowDiff} />
|
||||
<TabBar
|
||||
type="top"
|
||||
look="brand"
|
||||
|
|
@ -86,7 +89,7 @@ export function HistoryModal({ modalProps, message }: { modalProps: ModalProps;
|
|||
</TabBar>
|
||||
|
||||
<div className={classes(CodeContainerClasses.markup, MiscClasses.messageContent, Margins.top20)}>
|
||||
{parseEditContent(contents[currentTab], message, currentTab === contents.length - 1 ? undefined : contents[contents.length - 1])}
|
||||
{parseEditContent(contents[currentTab], message, showDiff ? currentTab === contents.length - 1 ? undefined : contents[contents.length - 1] : undefined)}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ export function parseEditContent(content: string, message: Message, previousCont
|
|||
});
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
export const settings = definePluginSettings({
|
||||
deleteStyle: {
|
||||
type: OptionType.SELECT,
|
||||
description: "The style of deleted messages",
|
||||
|
|
@ -363,6 +363,11 @@ const settings = definePluginSettings({
|
|||
description: "Whether to ignore messages by yourself",
|
||||
default: false,
|
||||
},
|
||||
ignoreSelfEdits: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether to ignore edits by yourself",
|
||||
default: false,
|
||||
},
|
||||
ignoreUsers: {
|
||||
type: OptionType.STRING,
|
||||
description: "Comma-separated list of user IDs to ignore",
|
||||
|
|
@ -402,6 +407,11 @@ const settings = definePluginSettings({
|
|||
return !this.store.showEditDiffs;
|
||||
},
|
||||
},
|
||||
ignoreSelfEdits: {
|
||||
disabled() {
|
||||
return this.store.ignoreSelf;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
|
|
@ -585,6 +595,7 @@ export default definePlugin({
|
|||
const {
|
||||
ignoreBots,
|
||||
ignoreSelf,
|
||||
ignoreSelfEdits,
|
||||
ignoreUsers,
|
||||
ignoreChannels,
|
||||
ignoreGuilds,
|
||||
|
|
@ -596,6 +607,7 @@ export default definePlugin({
|
|||
return (
|
||||
(ignoreBots && message.author?.bot) ||
|
||||
(ignoreSelf && message.author?.id === myId) ||
|
||||
(ignoreSelfEdits && isEdit && message.author?.id === myId) ||
|
||||
ignoreUsers.includes(message.author?.id) ||
|
||||
ignoreChannels.includes(message.channel_id) ||
|
||||
ignoreChannels.includes(
|
||||
|
|
|
|||
|
|
@ -138,8 +138,7 @@ export default definePlugin({
|
|||
{
|
||||
find: "MessageReactions.render:",
|
||||
replacement: {
|
||||
// FIXME: (?:medium|normal) is for stable compat
|
||||
match: /tag:"strong",variant:"text-md\/(?:medium|normal)"(?<=onContextMenu:.{0,15}\((\i),(\i),\i\).+?)/,
|
||||
match: /tag:"strong",variant:"text-md\/medium"(?<=onContextMenu:.{0,15}\((\i),(\i),\i\).+?)/,
|
||||
replace: "$&,style:$self.getColorStyle($2?.id,$1?.channel?.id)"
|
||||
},
|
||||
predicate: () => settings.store.reactorsList,
|
||||
|
|
|
|||
|
|
@ -35,10 +35,8 @@ export default definePlugin({
|
|||
Component: StartupTimingPage,
|
||||
Icon: ClockIcon
|
||||
});
|
||||
SettingsPlugin.settingsSectionMap.push(["VencordStartupTimings", "vencord_startup_timings"]);
|
||||
},
|
||||
stop() {
|
||||
removeFromArray(SettingsPlugin.customEntries, e => e.key === "vencord_startup_timings");
|
||||
removeFromArray(SettingsPlugin.settingsSectionMap, entry => entry[1] === "vencord_startup_timings");
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,17 +46,6 @@ export default definePlugin({
|
|||
// if (inlinedCalculatePlayingCount(a,b) >= limit) return;
|
||||
match: /(BURST_REACTION_EFFECT_PLAY:\i=>{.+?if\()(\(\(\i,\i\)=>.+?\(\i,\i\))>=5+?(?=\))/,
|
||||
replace: (_, rest, playingCount) => `${rest}!$self.shouldPlayBurstReaction(${playingCount})`
|
||||
},
|
||||
// FIXME(Bundler agressive inline): Remove the non used compability once enough time has passed
|
||||
{
|
||||
/*
|
||||
* var limit = 5
|
||||
* ...
|
||||
* if (calculatePlayingCount(a,b) >= limit) return;
|
||||
*/
|
||||
match: /((\i)=5.+?)if\((.{0,20}?)>=\2\)return;/,
|
||||
replace: (_, rest, playingCount) => `${rest}if(!$self.shouldPlayBurstReaction(${playingCount}))return;`,
|
||||
noWarn: true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1280,6 +1280,10 @@ export const EquicordDevs = Object.freeze({
|
|||
name: "ScattrdBlade",
|
||||
id: 678007540608532491n
|
||||
},
|
||||
Ape: {
|
||||
name: "Ape",
|
||||
id: 722832640360841308n,
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@
|
|||
|
||||
import { AudioProcessor } from "@api/AudioPlayer";
|
||||
import type { ProfileBadge } from "@api/Badges";
|
||||
import type { ChatBarButtonData, ChatBarButtonFactory } from "@api/ChatButtons";
|
||||
import type { ChatBarButtonData } from "@api/ChatButtons";
|
||||
import type { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { HeaderBarButtonData } from "@api/HeaderBar";
|
||||
import type { MemberListDecoratorFactory } from "@api/MemberListDecorators";
|
||||
import type { MessageAccessoryFactory } from "@api/MessageAccessories";
|
||||
import type { MessageDecorationFactory } from "@api/MessageDecorations";
|
||||
import type { MessageClickListener, MessageEditListener, MessageSendListener } from "@api/MessageEvents";
|
||||
import type { MessagePopoverButtonData, MessagePopoverButtonFactory } from "@api/MessagePopover";
|
||||
import type { MessagePopoverButtonData } from "@api/MessagePopover";
|
||||
import type { NicknameIconFactory } from "@api/NicknameIcons";
|
||||
import { ProfileCollectionFactory } from "@api/ProfileCollections";
|
||||
import type { UserAreaButtonData } from "@api/UserArea";
|
||||
|
|
@ -211,16 +211,6 @@ export interface PluginDef {
|
|||
userAreaButton?: UserAreaButtonData;
|
||||
renderProfileCollection?: ProfileCollectionFactory;
|
||||
|
||||
// TODO: Remove eventually
|
||||
/**
|
||||
* @deprecated Use {@link chatBarButton} instead
|
||||
*/
|
||||
renderChatBarButton?: ChatBarButtonFactory;
|
||||
/**
|
||||
* @deprecated Use {@link messagePopoverButton} instead
|
||||
*/
|
||||
renderMessagePopoverButton?: MessagePopoverButtonFactory;
|
||||
|
||||
/**
|
||||
* A Vencord plugin that is modified for extra features in Equicord
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ export const IconUtils: t.IconUtils = findByPropsLazy("getGuildBannerURL", "getU
|
|||
|
||||
export const ColorUtils = mapMangledModuleLazy("Invalid hex color format", {
|
||||
rgbToHex: filters.byCode(".toString(16).slice(1)"),
|
||||
hexToRgba: filters.byCode("rgba(", "??"),
|
||||
hexToRgba: filters.byCode("`rgba(${"),
|
||||
hexToRgb: filters.byCode(".rgb();return"),
|
||||
rgbToHsl: filters.byCode("saturation:", "lightness:"),
|
||||
mixColors: filters.byCode(".substring(1,3),16)"),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue