diff --git a/package.json b/package.json index 8c3219844..a91ed86b8 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index bcaf77362..8138fc0cf 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -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); }); + } } } diff --git a/scripts/depCheck.mjs b/scripts/depCheck.mjs new file mode 100644 index 000000000..0a333a900 --- /dev/null +++ b/scripts/depCheck.mjs @@ -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"); +} diff --git a/src/api/PluginManager.ts b/src/api/PluginManager.ts index 5a06dbd8b..0721b4d20 100644 --- a/src/api/PluginManager.ts +++ b/src/api/PluginManager.ts @@ -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 = [ "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 diff --git a/src/api/ProfileCollections.tsx b/src/api/ProfileCollections.tsx index 30cd403cd..2a8a16783 100644 --- a/src/api/ProfileCollections.tsx +++ b/src/api/ProfileCollections.tsx @@ -59,7 +59,7 @@ function ProfileCollections({ props }: { props: any; }) { return Array.from(profileCollections) .map(([id, { render: Collection }]) => ( logger.error(`Failed to render profile collection: ${id}`, e.error)}> - {Collection({ ...props, user: props.user ?? props.currentUser })} + )); } diff --git a/src/equicordplugins/betterAudioPlayer/index.tsx b/src/equicordplugins/betterAudioPlayer/index.tsx index 7c0204ecd..1692b2d85 100644 --- a/src/equicordplugins/betterAudioPlayer/index.tsx +++ b/src/equicordplugins/betterAudioPlayer/index.tsx @@ -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; + 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): 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, 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, 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 { + 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; src: string; }) { + const canvasRef = React.useRef(null); + const audioCtxRef = React.useRef(null); + const analyserRef = React.useRef(null); + const animFrameRef = React.useRef(0); + const setupDoneRef = React.useRef(false); + const blobUrlRef = React.useRef(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 | null = null; + let frequencyData: Uint8Array | 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 ( + + ); } 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 ; }, }); diff --git a/src/equicordplugins/betterAudioPlayer/style.css b/src/equicordplugins/betterAudioPlayer/styles.css similarity index 69% rename from src/equicordplugins/betterAudioPlayer/style.css rename to src/equicordplugins/betterAudioPlayer/styles.css index 7ce03332a..2bfaba0fd 100644 --- a/src/equicordplugins/betterAudioPlayer/style.css +++ b/src/equicordplugins/betterAudioPlayer/styles.css @@ -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; +} \ No newline at end of file diff --git a/src/equicordplugins/channelTabs/components/ChannelTab.tsx b/src/equicordplugins/channelTabs/components/ChannelTab.tsx index 7b953b028..567f48e9d 100644 --- a/src/equicordplugins/channelTabs/components/ChannelTab.tsx +++ b/src/equicordplugins/channelTabs/components/ChannelTab.tsx @@ -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( diff --git a/src/equicordplugins/characterCounter/index.tsx b/src/equicordplugins/characterCounter/index.tsx deleted file mode 100644 index 3f2fb0a10..000000000 --- a/src/equicordplugins/characterCounter/index.tsx +++ /dev/null @@ -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 ( -
- {length}/ - {charMax} -
- ); - }, { noop: true }) -}); diff --git a/src/equicordplugins/components.dev/index.tsx b/src/equicordplugins/components.dev/index.tsx index a8269b4bc..b740b2403 100644 --- a/src/equicordplugins/components.dev/index.tsx +++ b/src/equicordplugins/components.dev/index.tsx @@ -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"); }, }); diff --git a/src/equicordplugins/declutter/index.tsx b/src/equicordplugins/declutter/index.tsx index dad0e2560..e816d9159 100644 --- a/src/equicordplugins/declutter/index.tsx +++ b/src/equicordplugins/declutter/index.tsx @@ -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;" diff --git a/src/equicordplugins/dragify/index.tsx b/src/equicordplugins/dragify/index.tsx index 183f67d1e..a1105f878 100644 --- a/src/equicordplugins/dragify/index.tsx +++ b/src/equicordplugins/dragify/index.tsx @@ -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)," } }, diff --git a/src/equicordplugins/equibopStreamFixes.equibop/index.tsx b/src/equicordplugins/equibopStreamFixes.equibop/index.tsx new file mode 100644 index 000000000..fb9a99b3f --- /dev/null +++ b/src/equicordplugins/equibopStreamFixes.equibop/index.tsx @@ -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; + }, +}); diff --git a/src/equicordplugins/fileUpload/constants.ts b/src/equicordplugins/fileUpload/constants.ts index 9742e2dbe..5934a3be9 100644 --- a/src/equicordplugins/fileUpload/constants.ts +++ b/src/equicordplugins/fileUpload/constants.ts @@ -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)}`; } diff --git a/src/equicordplugins/fileUpload/settings.tsx b/src/equicordplugins/fileUpload/settings.tsx index acebee402..b1aceb041 100644 --- a/src/equicordplugins/fileUpload/settings.tsx +++ b/src/equicordplugins/fileUpload/settings.tsx @@ -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() { /> + store.corsProxyUrl = v} + placeholder="https://your-cors-proxy.example.com" + /> + + + codeberg.org/key/corsproxy + + 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 ; + return ; }, isTyping() { return useStateFromStores([TypingStore], () => diff --git a/src/equicordplugins/iRememberYou/index.tsx b/src/equicordplugins/iRememberYou/index.tsx index 8b4614331..dc9f62b57 100644 --- a/src/equicordplugins/iRememberYou/index.tsx +++ b/src/equicordplugins/iRememberYou/index.tsx @@ -31,7 +31,6 @@ export default definePlugin({ Component: () => , 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); diff --git a/src/equicordplugins/iconViewer/index.tsx b/src/equicordplugins/iconViewer/index.tsx index f2135cbf2..1f364adf5 100644 --- a/src/equicordplugins/iconViewer/index.tsx +++ b/src/equicordplugins/iconViewer/index.tsx @@ -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"); }, }); diff --git a/src/equicordplugins/loginWithQR/index.tsx b/src/equicordplugins/loginWithQR/index.tsx index d0c36ccc6..21f1c7c9d 100644 --- a/src/equicordplugins/loginWithQR/index.tsx +++ b/src/equicordplugins/loginWithQR/index.tsx @@ -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(); }, }); diff --git a/src/equicordplugins/musicControls/settings.tsx b/src/equicordplugins/musicControls/settings.tsx index d79fe73dd..2515c0278 100644 --- a/src/equicordplugins/musicControls/settings.tsx +++ b/src/equicordplugins/musicControls/settings.tsx @@ -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 ( + <> + +