diff --git a/README.md b/README.md index 99c4c99a..329a364a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch ### Extra included plugins
-152 additional plugins +163 additional plugins ### All Platforms @@ -23,6 +23,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - AtSomeone by Joona - BannersEverywhere by ImLvna & AutumnVN - BetterActivities by D3SOX, Arjix, AutumnVN +- BetterAudioPlayer by creations - BetterBanReasons by Inbestigator - BetterBlockedUsers by TheArmagan & Elvyra - BetterInvites by iamme @@ -42,6 +43,9 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - CustomSounds by TheKodeToad & SpikeHD - CustomTimestamps by Rini & nvhrr - CustomUserColors by mochienya +- CuteAnimeBoys by ShadyGoat +- CuteNekos by echo +- CutePats by thororen - DecodeBase64 by ThePirateStoner - Demonstration by Samwich - DisableAnimations by S€th @@ -64,10 +68,13 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - FriendshipRanks by Samwich - FriendTags by Samwich - FullVcPfp by mochie +- GensokyoRadioRPC by RyanCaoDev & Prince527 - GifCollections by Aria & Creations - GifRoulette by Samwich - GitHubRepos by talhakf +- Glide by Samwich - GlobalBadges by HypedDomi & Hosted by Wolfie +- GoogleThat by Samwich - HideChatButtons by iamme - HideServers by bepvte - HolyNotes by Wolfie @@ -75,6 +82,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - HopOn by ImLvna - Husk by nin0dev - IconViewer by iamme +- Identity by Samwich - IgnoreCalls by TheArmagan - IgnoreTerms by D3SOX - ImagePreview by Creations @@ -124,6 +132,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - RPCEditor by Nyako & nin0dev - RPCStats by Samwich - SearchFix by Jaxx +- SekaiStickers by MaiKokain - ServerSearch by camila314 - ShowBadgesInChat by Inbestigator & KrystalSkull - SidebarChat by Joona @@ -142,6 +151,8 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - Timezones by Aria - Title by Kyuuhachi - ToggleVideoBind by mochie +- TosuRPC by AutumnVN +- Translate+ by Prince527 & Ven - UnitConverter by sadan - UnlimitedAccounts by thororen - UnreadCountBadge by Joona diff --git a/src/components/ThemeSettings/ThemesTab.tsx b/src/components/ThemeSettings/ThemesTab.tsx index 72970530..0e752efc 100644 --- a/src/components/ThemeSettings/ThemesTab.tsx +++ b/src/components/ThemeSettings/ThemesTab.tsx @@ -318,12 +318,6 @@ function ThemesTab() { If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder. - - External Resources - For security reasons, loading resources (styles, fonts, images, ...) from most sites is blocked. - Make sure all your assets are hosted on GitHub, GitLab, Codeberg, Imgur, Discord or Google Fonts. - - <> diff --git a/src/equicordplugins/betterAudioPlayer/index.tsx b/src/equicordplugins/betterAudioPlayer/index.tsx new file mode 100644 index 00000000..0f942e90 --- /dev/null +++ b/src/equicordplugins/betterAudioPlayer/index.tsx @@ -0,0 +1,312 @@ +/* + * 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 { EquicordDevs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { showToast, Toasts } from "@webpack/common"; + +const fileSizeLimit = 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; +} + +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"); + + 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://corsproxy.io?" + 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); + } + + 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(); + }); +} + +function drawOscilloscope(canvasContext, canvas, dataArray, bufferLength) { + const sliceWidth = canvas.width / bufferLength; + 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(); + + for (let i = 0; i < bufferLength; i++) { + const v = dataArray[i] / 128.0; + const y = (v * canvas.height) / 2; + + if (oscilloscopeSolidColor) { + canvasContext.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); + } + + x += sliceWidth; + } + + canvasContext.stroke(); +} + +function drawSpectrograph(canvasContext, canvas, frequencyData, bufferLength) { + const { spectrographSolidColor, spectrographColor } = settings.store; + const maxHeight = canvas.height; + const barWidth = canvas.width / bufferLength; + 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; + + if (spectrographSolidColor) { + canvasContext.fillStyle = `rgb(${spectrographColor})`; + } 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); + 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; + } + + canvasContext.fillRect(x, canvas.height - normalizedHeight, barWidth, normalizedHeight); + 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); + + if (!metadata) return; + + addListeners(metadata.audio, metadata.url, metadata.parentBorderRadius); + }); +} + +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); + } + }); + } + }); + }); + observer.observe(targetNode, { + childList: true, + subtree: true, + }); +} + +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; +} + +function handleColorChange(value, settingKey, defaultValue) { + const rgbPattern = /^(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})$/; + + if (!value.match(rgbPattern)) { + const rgb = tryHexToRgb(value); + + 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; + } +} + +const settings = definePluginSettings({ + oscilloscope: { + type: OptionType.BOOLEAN, + description: "Enable oscilloscope visualizer", + default: true, + }, + spectrograph: { + type: OptionType.BOOLEAN, + description: "Enable spectrograph visualizer", + default: true, + }, + oscilloscopeSolidColor: { + type: OptionType.BOOLEAN, + description: "Use solid color for oscilloscope", + default: false, + }, + oscilloscopeColor: { + type: OptionType.STRING, + description: "Color for oscilloscope", + default: "255, 255, 255", + onChange: value => handleColorChange(value, "oscilloscopeColor", "255, 255, 255"), + }, + spectrographSolidColor: { + type: OptionType.BOOLEAN, + description: "Use solid color for spectrograph", + default: false, + }, + spectrographColor: { + type: OptionType.STRING, + description: "Color for spectrograph", + 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, + }, +}); + +export default definePlugin({ + name: "BetterAudioPlayer", + 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); + createObserver(targetNode as HTMLElement); + } else { + requestAnimationFrame(waitForContent); + } + }; + waitForContent(); + }, +}); diff --git a/src/equicordplugins/betterAudioPlayer/style.css b/src/equicordplugins/betterAudioPlayer/style.css new file mode 100644 index 00000000..7ce03332 --- /dev/null +++ b/src/equicordplugins/betterAudioPlayer/style.css @@ -0,0 +1,10 @@ +.better-audio-visualizer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1; + border: none; +} diff --git a/src/equicordplugins/cuteAnimeBoys/index.ts b/src/equicordplugins/cuteAnimeBoys/index.ts new file mode 100644 index 00000000..3f86d8e9 --- /dev/null +++ b/src/equicordplugins/cuteAnimeBoys/index.ts @@ -0,0 +1,60 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { EquicordDevs } from "@utils/constants"; + +import { ApplicationCommandOptionType } from "../../api/Commands"; +import definePlugin from "../../utils/types"; + +function rand(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +async function fetchReddit(sub: string) { + const res = await fetch(`https://www.reddit.com/r/${sub}/top.json?limit=100&t=all`); + const resp = await res.json(); + try { + const { children } = resp.data; + const r = rand(0, children.length - 1); + return children[r].data.url; + } catch (err) { + console.error(resp); + console.error(err); + } + return ""; +} + +export default definePlugin({ + name: "CuteAnimeBoys", + authors: [EquicordDevs.ShadyGoat], + description: "Add a command to send cute anime boys in the chat", + commands: [{ + name: "anime-boys", + description: "Send cute anime boys", + options: [ + { + name: "cat", + description: "If set, this will send exclusively cute anime cat boys", + type: ApplicationCommandOptionType.BOOLEAN, + required: false, + }, + ], + + async execute(args) { + let sub = "cuteanimeboys"; + if (args.length > 0) { + const v = args[0].value as any as boolean; + if (v) { + sub = "animecatboys"; + } + } + + return { + content: await fetchReddit(sub), + }; + }, + }] +}); diff --git a/src/equicordplugins/cuteNekos/index.ts b/src/equicordplugins/cuteNekos/index.ts new file mode 100644 index 00000000..e7d7a0f5 --- /dev/null +++ b/src/equicordplugins/cuteNekos/index.ts @@ -0,0 +1,29 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +async function getcuteneko(): Promise { + const res = await fetch("https://nekos.best/api/v2/neko"); + const url = (await res.json()).results[0].url as string; + return url; +} + + + +export default definePlugin({ + name: "CuteNekos", + authors: [Devs.echo], + description: "Send Nekos to others", + commands: [{ + name: "nekos", + description: "Send Neko", + execute: async opts => ({ + content: await getcuteneko() + }) + }] +}); diff --git a/src/equicordplugins/cutePats/index.ts b/src/equicordplugins/cutePats/index.ts new file mode 100644 index 00000000..5ffaabde --- /dev/null +++ b/src/equicordplugins/cutePats/index.ts @@ -0,0 +1,29 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors* + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { EquicordDevs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +async function getcutepats(): Promise { + const res = await fetch("https://api.waifu.pics/sfw/pat"); + const url = (await res.json()).url as string; + return url; +} + + + +export default definePlugin({ + name: "CutePats", + authors: [EquicordDevs.thororen], + description: "Sending Head Pats", + commands: [{ + name: "pat", + description: "Sends a headpat gif", + execute: async opts => ({ + content: await getcutepats() + }) + }] +}); diff --git a/src/equicordplugins/gensokyoRadioRPC/index.tsx b/src/equicordplugins/gensokyoRadioRPC/index.tsx new file mode 100644 index 00000000..4a166406 --- /dev/null +++ b/src/equicordplugins/gensokyoRadioRPC/index.tsx @@ -0,0 +1,102 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { Devs, EquicordDevs } from "@utils/constants"; +import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types"; +import { ApplicationAssetUtils, FluxDispatcher, Forms } from "@webpack/common"; + +import { Activity, ActivityFlag, ActivityType } from "./types"; + +const Native = VencordNative.pluginHelpers.GensokyoRadioRPC as PluginNative; + +const applicationId = "1253772057926303804"; + +function setActivity(activity: Activity | null) { + FluxDispatcher.dispatch({ + type: "LOCAL_ACTIVITY_UPDATE", + activity, + socketId: "GensokyoRadio", + }); +} + +function getImageAsset(data: string) { + return ApplicationAssetUtils.fetchAssetIds(applicationId, [data]).then(ids => ids[0]); +} + +const settings = definePluginSettings({ + refreshInterval: { + type: OptionType.SLIDER, + description: "The interval between activity refreshes (seconds)", + markers: [1, 2, 2.5, 3, 5, 10, 15], + default: 15, + restartNeeded: true, + } +}); + +export default definePlugin({ + name: "GensokyoRadioRPC", + description: "Discord rich presence for Gensokyo Radio!", + authors: [Devs.RyanCaoDev, EquicordDevs.Prince527], + reporterTestable: ReporterTestable.None, + + settingsAboutComponent() { + return <> + + Discord rich presence for Gensokyo Radio! + + ; + }, + + settings, + + start() { + this.updatePresence(); + this.updateInterval = setInterval(() => { this.updatePresence(); }, settings.store.refreshInterval * 1000); + }, + + stop() { + clearInterval(this.updateInterval); + FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null }); + }, + + updatePresence() { + this.getActivity().then(activity => { setActivity(activity); }); + }, + + async getActivity(): Promise { + const trackData = await Native.fetchTrackData(); + if (!trackData) return null; + + return { + application_id: applicationId, + + name: "Gensokyo Radio", + details: trackData.title, + state: trackData.artist, + + timestamps: { + // start: Date.now() - (trackData.position * 1000), + start: trackData.position * 1000, + // end: Date.now() - (trackData.position * 1000) + (trackData.duration * 1000), + end: trackData.duration * 1000, + }, + + assets: { + large_image: await getImageAsset(trackData.artwork), + large_text: trackData.album, + small_image: await getImageAsset("logo"), + small_text: "Gensokyo Radio" + }, + + buttons: undefined, + metadata: { button_urls: undefined }, + + type: ActivityType.LISTENING, + flags: ActivityFlag.INSTANCE, + }; + } +}); diff --git a/src/equicordplugins/gensokyoRadioRPC/native.ts b/src/equicordplugins/gensokyoRadioRPC/native.ts new file mode 100644 index 00000000..69dbe37a --- /dev/null +++ b/src/equicordplugins/gensokyoRadioRPC/native.ts @@ -0,0 +1,20 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import type { TrackData } from "./types"; + +export async function fetchTrackData(): Promise { + const song = await (await fetch("https://gensokyoradio.net/api/station/playing/")).json(); + + return { + title: song.SONGINFO.TITLE, + album: song.SONGINFO.ALBUM, + artist: song.SONGINFO.ARTIST, + position: song.SONGTIMES.SONGSTART, + duration: song.SONGTIMES.SONGEND, + artwork: song.MISC.ALBUMART ? `https://gensokyoradio.net/images/albums/500/${song.MISC.ALBUMART}` : "undefined", + }; +} diff --git a/src/equicordplugins/gensokyoRadioRPC/types.ts b/src/equicordplugins/gensokyoRadioRPC/types.ts new file mode 100644 index 00000000..3fa0464b --- /dev/null +++ b/src/equicordplugins/gensokyoRadioRPC/types.ts @@ -0,0 +1,57 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface ActivityAssets { + large_image?: string; + large_text?: string; + small_image?: string; + small_text?: string; +} + +export interface Activity { + state: string; + details?: string; + timestamps?: { + start?: number; + end?: number; + }; + assets?: ActivityAssets; + buttons?: Array; + name: string; + application_id: string; + metadata?: { + button_urls?: Array; + }; + type: number; + flags: number; +} + +export interface ActivityAssets { + large_image?: string; + large_text?: string; + small_image?: string; + small_text?: string; +} + +export const enum ActivityType { + PLAYING = 0, + LISTENING = 2, +} + +export const enum ActivityFlag { + INSTANCE = 1 << 0 +} + +export interface TrackData { + title: string; + album: string; + artist: string; + + artwork: string; + + position: number; + duration: number; +} diff --git a/src/equicordplugins/glide/crosshair.png b/src/equicordplugins/glide/crosshair.png new file mode 100644 index 00000000..58e57362 Binary files /dev/null and b/src/equicordplugins/glide/crosshair.png differ diff --git a/src/equicordplugins/glide/generateTheme.tsx b/src/equicordplugins/glide/generateTheme.tsx new file mode 100644 index 00000000..d3ca85f0 --- /dev/null +++ b/src/equicordplugins/glide/generateTheme.tsx @@ -0,0 +1,30 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export function generateRandomColorHex(): string { + const r = Math.floor(Math.random() * 90); + const g = Math.floor(Math.random() * 90); + const b = Math.floor(Math.random() * 90); + + return `${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; +} + +export function darkenColorHex(color: string): string { + const hex = color.replace(/^#/, ""); + const bigint = parseInt(hex, 16); + let r = (bigint >> 16) & 255; + let g = (bigint >> 8) & 255; + let b = bigint & 255; + r = Math.max(r - 5, 0); + g = Math.max(g - 5, 0); + b = Math.max(b - 5, 0); + return `${((r << 16) + (g << 8) + b).toString(16).padStart(6, "0")}`; +} + +export function saturateColorHex(color: string): string { + // i should really do something with this at some point :P + return color; +} diff --git a/src/equicordplugins/glide/index.tsx b/src/equicordplugins/glide/index.tsx new file mode 100644 index 00000000..5547133a --- /dev/null +++ b/src/equicordplugins/glide/index.tsx @@ -0,0 +1,801 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings, Settings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType, StartAt } from "@utils/types"; +import { findComponentByCodeLazy } from "@webpack"; +import { Button, Clipboard, Forms, TextInput, Toasts, useState } from "@webpack/common"; + +import { darkenColorHex, generateRandomColorHex, saturateColorHex } from "./generateTheme"; +import { themes } from "./themeDefinitions"; + +export interface ThemePreset { + bgcol: string; + accentcol: string; + textcol: string; + brand: string; + name: string; +} + +let setPreset; + + +function LoadPreset(preset?: ThemePreset) { + if (setPreset === settings.store.ColorPreset) { return; } + const theme: ThemePreset = preset == null ? themes[settings.store.ColorPreset] : preset; + setPreset = settings.store.ColorPreset; + settings.store.Primary = theme.bgcol; + settings.store.Accent = theme.accentcol; + settings.store.Text = theme.textcol; + settings.store.Brand = theme.brand; + injectCSS(); +} + +function mute(hex, amount) { + hex = hex.replace(/^#/, ""); + const bigint = parseInt(hex, 16); + let r = (bigint >> 16) & 255; + let g = (bigint >> 8) & 255; + let b = bigint & 255; + r = Math.max(r - amount, 0); + g = Math.max(g - amount, 0); + b = Math.max(b - amount, 0); + return "#" + ((r << 16) + (g << 8) + b).toString(16).padStart(6, "0"); +} + +function copyPreset(name: string) { + const template = + ` +{ + bgcol: "${settings.store.Primary}", + accentcol: "${settings.store.Accent}", + textcol: "${settings.store.Text}", + brand: "${settings.store.Brand}", + name: "${name}" +} + `; + if (Clipboard.SUPPORTS_COPY) { + Clipboard.copy(template); + } + +} + +function CopyPresetComponent() { + + const [inputtedName, setInputtedName] = useState(""); + return ( + <> + + {"Preset name"} + + + + + + ); +} + +const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)"); + +export function generateAndApplyProceduralTheme() { + + const randomBackgroundColor = generateRandomColorHex(); + const accentColor = darkenColorHex(randomBackgroundColor); + const textColor = "ddd0d0"; + const brandColor = saturateColorHex(randomBackgroundColor); + + settings.store.Primary = randomBackgroundColor; + settings.store.Accent = accentColor; + settings.store.Text = textColor; + settings.store.Brand = brandColor; + + injectCSS(); +} + +const settings = definePluginSettings({ + serverListAnim: { + type: OptionType.BOOLEAN, + description: "Toggles if the server list hides when not hovered", + default: false, + onChange: () => injectCSS() + }, + memberListAnim: { + type: OptionType.BOOLEAN, + description: "Toggles if the member list hides when not hovered", + default: true, + onChange: () => injectCSS() + }, + privacyBlur: { + type: OptionType.BOOLEAN, + description: "Blurs potentially sensitive information when not tabbed in", + default: false, + onChange: () => injectCSS() + }, + tooltips: { + type: OptionType.BOOLEAN, + description: "If tooltips are displayed in the client", + default: false, + onChange: () => injectCSS() + }, + customFont: { + type: OptionType.STRING, + description: "The google fonts @import for a custom font (blank to disable)", + default: "@import url('https://fonts.googleapis.com/css2?family=Poppins&wght@500&display=swap');", + onChange: injectCSS + }, + animationSpeed: { + type: OptionType.STRING, + description: "The speed of animations", + default: "0.2", + onChange: injectCSS + }, + colorsEnabled: { + type: OptionType.BOOLEAN, + description: "Whether or not to enable theming", + onChange: () => injectCSS() + }, + ColorPreset: { + type: OptionType.SELECT, + description: "Some pre-made color presets (more soon hopefully)", + options: themes.map(theme => ({ label: theme.name, value: themes.indexOf(theme), default: themes.indexOf(theme) === 0 })), + onChange: () => { LoadPreset(); } + }, + Primary: { + type: OptionType.COMPONENT, + description: "", + default: "000000", + component: () => + }, + Accent: { + type: OptionType.COMPONENT, + description: "", + default: "313338", + component: () => + }, + Text: { + type: OptionType.COMPONENT, + description: "", + default: "ffffff", + component: () => + }, + Brand: { + type: OptionType.COMPONENT, + description: "", + default: "ffffff", + component: () => + }, + pastelStatuses: { + type: OptionType.BOOLEAN, + description: "Changes the status colors to be more pastel (fits with the catppuccin presets)", + default: true, + onChange: () => injectCSS() + }, + DevTools: + { + type: OptionType.COMPONENT, + description: "meow", + default: "", + component: () => + }, + ExportTheme: + { + type: OptionType.COMPONENT, + description: "", + default: "", + component: () => + } +}); + + +export function ColorPick({ propertyname }: { propertyname: string; }) { + return ( + +
+ {propertyname} + + { + const hexColor = color.toString(16).padStart(6, "0"); + settings.store[propertyname] = hexColor; + injectCSS(); + } + } + showEyeDropper={false} + /> +
+ ); +} + + +function copyCSS() { + if (Clipboard.SUPPORTS_COPY) { + Clipboard.copy(getCSS(parseFontContent())); + } +} + +function parseFontContent() { + const fontRegex = /family=([^&;,:]+)/; + const customFontString: string = Settings.plugins.Glide.customFont; + if (customFontString == null) { return; } + const fontNameMatch: RegExpExecArray | null = fontRegex.exec(customFontString); + const fontName = fontNameMatch ? fontNameMatch[1].replace(/[^a-zA-Z0-9]+/g, " ") : ""; + return fontName; +} +function injectCSS() { + if (Settings.plugins.Glide.enabled) { + + const fontName = parseFontContent(); + const theCSS = getCSS(fontName); + + const elementToRemove = document.getElementById("GlideStyleInjection"); + if (elementToRemove) { + elementToRemove.remove(); + } + const styleElement = document.createElement("style"); + styleElement.id = "GlideStyleInjection"; + styleElement.textContent = theCSS; + document.documentElement.appendChild(styleElement); + } +} + +function getCSS(fontName) { + return ` + /* IMPORTS */ + + /* Fonts */ + @import url('https://fonts.googleapis.com/css2?family=Nunito&display=swap'); + @import url('https://fonts.googleapis.com/css2?family=Fira+Code&display=swap'); + ${Settings.plugins.Glide.customFont} + + /*Settings things*/ + /*Server list animation*/ + ${Settings.plugins.Glide.serverListAnim ? ` + .guilds_a4d4d9 { + width: 10px; + transition: width var(--animspeed) ease 0.1s, opacity var(--animspeed) ease 0.1s; + opacity: 0; + } + .guilds_a4d4d9:hover { + width: 65px; + opacity: 100; + } + ` : ""} + /*Member list anim toggle*/ + ${Settings.plugins.Glide.memberListAnim ? ` + .container_cbd271 + { + width: 60px; + opacity: 0.2; + transition: width var(--animspeed) ease 0.1s, opacity var(--animspeed) ease 0.1s; + + } + .container_cbd271:hover + { + width: 250px; + opacity: 1; + } + ` : ""} + /*Privacy blur*/ + ${Settings.plugins.Glide.privacyBlur ? ` + .header_f9f2ca, + .container_ee69e0, + .title_a7d72e, + .layout_ec8679, + [aria-label="Members"] { + filter: blur(0); + transition: filter 0.2s ease-in-out; + } + + body:not(:hover) .header_f9f2ca, + body:not(:hover) .container_ee69e0, + body:not(:hover) .title_a7d72e, + body:not(:hover) [aria-label="Members"], + body:not(:hover) .layout_ec8679 { + filter: blur(5px); + } + ` : ""} + /*Tooltips*/ + [class*="tooltip"] + { + ${!Settings.plugins.Glide.tooltips ? "display: none !important;" : ""} + } + /*Root configs*/ + :root + { + --animspeed: ${Settings.plugins.Glide.animationSpeed + "s"}; + --font-primary: ${(fontName.length > 0 ? fontName : "Nunito")}; + ${Settings.plugins.Glide.colorsEnabled ? ` + --accent: #${Settings.plugins.Glide.Accent}; + --bgcol: #${Settings.plugins.Glide.Primary}; + --text: #${Settings.plugins.Glide.Text}; + --brand: #${Settings.plugins.Glide.Brand}; + --mutedtext: ${mute(Settings.plugins.Glide.Text, 20)}; + --mutedbrand: ${mute(Settings.plugins.Glide.Brand, 10)}; + --mutedaccent: ${mute(Settings.plugins.Glide.Accent, 10)}; + ` : ""} + } +:root +{ + + /*VARIABLES*/ + + /*editable variables. Feel free to mess around with these to your hearts content, i recommend not editing the logic variables unless you have an understanding of css*/ + --glowcol: rgba(0, 0, 0, 0); + --mentioncol: rgb(0, 0, 0); + --mentionhighlightcol: rgb(0, 0, 0); + --linkcol: rgb(95, 231, 255); + --highlightcol: rgb(95, 231, 255); + + + + /*COLOR ASSIGNING (most of these probably effect more than whats commented)*/ + ${Settings.plugins.Glide.colorsEnabled ? ` + /*accent based*/ + + /*buttons*/ + --button-secondary-background: var(--accent); + + /*also buttons*/ + --brand-experiment: var(--brand); + --brand-experiment-560: var(--brand); + --brand-500: var(--brand); + + /*message bar*/ + --channeltextarea-background: var(--accent); + + /*selected dm background*/ + --background-modifier-selected: var(--accent); + + /*emoji autofill*/ + --primary-630: var(--accent); + + /*plugin grid square and nitro shop*/ + --background-secondary-alt: var(--accent); + + /*modal background, self explanatory*/ + --modal-background: var(--accent); + + /*color of the background of mention text*/ + --mention-background: var(--accent); + --input-background: var(--accent); + + /*the side profile thingy*/ + --profile-body-background-color: var(--accent); + + /*the weird hover thing idk*/ + --background-modifier-hover: var(--mutedaccent) !important; + + + /*background based*/ + + /*primary color, self explanatory*/ + --background-primary: var(--bgcol); + + /*dm list*/ + --background-secondary: var(--bgcol); + + /*outer frame and search background*/ + --background-tertiary: var(--bgcol); + + + /*friends header bar*/ + --bg-overlay-2: var(--bgcol); + + /*user panel*/ + --bg-overlay-1: var(--bgcol); + + /*call thingy*/ + --bg-overlay-app-frame: var(--bgcol); + + /*shop*/ + --background-mentioned-hover: var(--bgcol) !important; + --background-mentioned: var(--bgcol) !important; + + + + + /*other*/ + + /*mention side line color color*/ + --info-warning-foreground: var(--mentionhighlightcol); + + /*text color of mention text*/ + --mention-foreground: white; + + /*Link color*/ + --text-link: var(--linkcol); + --header-primary: var(--text); + --header-secondary: var(--text); + --font-display: var(--text); + --text-normal: var(--text); + --text-muted: var(--mutedtext); + --channels-default: var(--mutedtext); + --interactive-normal: var(--text) !important; + --white-500: var(--text); + +} + + + /*EXTRA COLORS*/ + + [class*="tooltipPrimary__"] + { + background-color: var(--mutedaccent) !important; + } + [class*="tooltipPointer_"] + { + border-top-color: var(--mutedaccent) !important; + } + /*sorry, forgot to document what these are when i was adding them*/ + .inspector_c3120f, .scroller_d53d65, .unicodeShortcut_dfa278 + { + background-color: var(--bgcol); + } + .inner_effbe2 + { + background-color: var(--accent); + } + /*recolor embeds*/ + [class^="embedWrap"] + { + border-color: var(--accent) !important; + background: var(--accent); + } + /*emoji menu recolor*/ + .contentWrapper_af5dbb, .header_a3bc57 + { + background-color: var(--bgcol); + } + /*vc background recolor*/ + .root_dd069c + { + background-color: var(--bgcol); + } + + /*Fix the forum page*/ + /*Set the bg color*/ + .container_a6d69a + { + background-color: var(--bgcol); + } + /*Recolor the posts to the accent*/ + .container_d331f1 + { + background-color: var(--accent); + } + + /*Recolor the background of stickers in the sticker picker that dont take up the full 1:1 ratio*/ + [id^="sticker-picker-grid"] + { + background-color: var(--bgcol); + } + /* profile sidebar*/ + [class="none_eed6a8 scrollerBase_eed6a8"] + { + background-color: var(--bgcol) !important; + } + /*Recolor the emoji, gif, and sticker picker selected button*/ + .navButtonActive_af5dbb, .stickerCategoryGenericSelected_a7a485, .categoryItemDefaultCategorySelected_dfa278 + { + background-color: var(--accent) !important; + } + + /*side profile bar*/ + [class="none_c49869 scrollerBase_c49869"] + { + background-color: var(--bgcol) !important; + } + .userPanelOverlayBackground_a2b6ae, .badgeList_ab525a + { + background-color: var(--accent) !important; + border-radius: 15px !important; + } + /*uhhhhhhhhhhhhhhh*/ + .headerText_c47fa9 + { + color: var(--text) !important; + } + /*message bar placeholder*/ + .placeholder_a552a6 + { + color: var(--mutedtext) !important + } + .menu_d90b3d + { + background: var(--accent) !important; + } + .messageGroupWrapper_ac90a2, .header_ac90a2 + { + background-color: var(--primary); + } + ${settings.store.pastelStatuses ? + ` + /*Pastel statuses*/ + rect[fill='#23a55a'], svg[fill='#23a55a'] { + fill: #80c968 !important; + } + rect[fill='#f0b232'], svg[fill='#f0b232'] { + fill: #e7ca45 !important; + } + rect[fill='#f23f43'], svg[fill='#f23f43'] { + fill: #e0526c !important; + } + rect[fill='#80848e'], svg[fill='#80848e'] { + fill: #696e88 !important; + } + rect[fill='#593695'], svg[fill='#593695'] { + fill: #ac7de6 important; + } + ` : ""} + .name_d8bfb3 + { + color: var(--text) !important; + } + .unread_d8bfb3 + { + background-color: var(--text) !important; + }` : ""} + + /*ROUNDING (rounding)*/ + + /*round message bar, some buttons, dm list button, new messages notif bar, channel buttons, emoji menu search bar, context menus, account connections(in that order)*/ + .scrollableContainer_bdf0de, .button_dd4f85, .interactive_f5eb4b, .newMessagesBar_cf58b5, .link_d8bfb3, .searchBar_c6ee36, .menu_d90b3d, .connectedAccountContainer_ab12c6 + { + border-radius: 25px; + } + /*round emojis seperately (and spotify activity icons)*/ + [data-type="emoji"], [class*="Spotify"] + { + border-radius: 5px; + } + /*round gifs and stickers (and maybe images idk lmao), and embeds*/ + [class^="imageWr"], [data-type="sticker"], [class^="embed"] + { + border-radius: 20px; + } + + .item_d90b3d + { + border-radius: 15px; + } + + + + /*slightly move messages right when hovered*/ + .cozyMessage_d5deea + { + left: 0px; + + transition-duration: 0.2s; + } + .cozyMessage_d5deea:hover + { + left: 3px; + } + + + /*CONTENT (Typically changing values or hiding elements)*/ + + /*remove status text in user thing*/ + .panelSubtextContainer_b2ca13 + { + display: none !important; + } + /*Hide most of the ugly useless scrollbars*/ + ::-webkit-scrollbar + { + display:none; + } + + + /*Hide user profile button, the dm favourite, dm close, support, gift buttons, the now playing column, and the channel + favourite icons*/ + [aria-label="Hide User Profile"], .favoriteIcon_c91bad, .closeButton_c91bad, [href="https://support.discord.com"], .nowPlayingColumn_c2739c, button[aria-label="Send a gift"], .icon_d8bfb3, .iconContainer_d8bfb3 + { + display :none; + } + + /*yeet the shitty nitro and family link tabs that no one likes*/ + .channel_c91bad[aria-posinset="2"], + .familyCenterLinkButton_f0963d + { + display: none; + + } + /*Remove the buttons at the bottom of the user pop out (seriously, who wanted this?)*/ + .addFriendSection__413d3 + { + display: none; + } + + /*No more useless spotify activity header*/ + .headerContainer_c1d9fd + { + display: none; + } + /*hide sidebar connections*/ + .profilePanelConnections_b433b4 + { + display: none; + } + /*pad the message bar right slightly. Not sure what caused the buttons to flow out of it, might be something in the theme :shrug:*/ + .inner_bdf0de + { + padding-right: 10px; + } + + /*Yeet hypesquad badges (who cares)*/ + [aria-label*="HypeSquad"] + { + display: none !important; + } + + /*Hide icon on file uploading status*/ + .icon_f46c86 + { + display: none; + } + + /*hide the play button while a soundmoji is playing*/ + .playing_bf9443 [viewBox="0 0 24 24"] + { + display:none; + } + /*hide the public servers button on member list*/ + [aria-label="Explore Discoverable Servers"] + { + display: none; + } + /*fix context menu being not symmetrical*/ + .scroller_d90b3d + { + padding: 6px 8px !important; + } + /*Hide the icon that displays what platform the user is listening with on spotify status*/ + .platformIcon_c1d9fd + { + display: none !important; + } + /*hide the album name on spotify statuses (who cares)*/ + [class="state_c1d9fd ellipsis_c1d9fd textRow_c1d9fd"] + { + display: none; + } + /*space the connections a bit better*/ + .userInfoSection_a24910 + { + margin-bottom: 0px; + padding-bottom: 0px; + } + /*Space channels*/ + .containerDefault_f6f816 + { + padding-top: 5px; + } + + /*round banners in profile popout*/ + .banner_d5fdb1:not(.panelBanner_c3e427) + { + border-radius: 20px; + } + /*round the user popout*/ + .userPopoutOuter_c69a7b + { + border-radius: 25px; + } + /*round the inner profile popout*/ + [class="userPopoutInner_c69a7b userProfileInner_c69a7b userProfileInnerThemedWithBanner_c69a7b"]::before + { + border-radius: 20px; + } + .footer_be6801 + { + display: none !important; + } + + /*STYLING (Modification of content to fit the theme)*/ + + /*Round and scale down the users banner*/ + .panelBanner_c3e427 + { + border-radius: 20px; + transform: scale(0.95); + } + /*add a soft glow to message bar contents, user panel, dms, channel names (in that order)*/ + .inner_bdf0de .layout_ec8679, .name_d8bfb3 + { + filter: drop-shadow(0px 0px 3px var(--glowcol)); + } + [type="button"] + { + transition: all 0.1s ease-in-out; + } + [type="button"]:hover + { + filter: drop-shadow(0px 0px 3px var(--glowcol)); + } + + /*Change the font*/ + :root + { + --font-code: "Fira Code"; + } + + /*Round all status symbols. basically does what that one plugin does but easier (disabled because of a bug) + .pointerEvents_c51b4e + { + mask: url(#svg-mask-status-online); + } + */ + + /*pfp uploader crosshair*/ + .overlayAvatar_ba5b9e + { + background-image: url(https://raw.githubusercontent.com/Equicord/Equicord/main/src/equicordplugins/glide/crosshair.png); + background-repeat: no-repeat; + background-position-x: 50%; + background-position-y: 50%; + border-width: 2px; + } + + /*change highlighted text color*/ + ::selection + { + color: inherit; + background-color: transparent; + text-shadow: 0px 0px 2px var(--highlightcol); + } + /*hide the line between connections and note*/ + [class="connectedAccounts_ab12c6 userInfoSection_a24910"] + { + border-top: transparent !important; + } + .container_cebd1c:not(.checked_cebd1c) + { + background-color: var(--mutedbrand) !important; + } + .checked_cebd1c + { + background-color: var(--brand) !important; + } +`; +} + +export default definePlugin({ + name: "Glide", + description: "A sleek, rounded theme for discord.", + authors: [Devs.Samwich], + settings, + start() { + injectCSS(); + }, + stop() { + const injectedStyle = document.getElementById("GlideStyleInjection"); + if (injectedStyle) { + injectedStyle.remove(); + } + }, + startAt: StartAt.DOMContentLoaded, + // preview thing, kinda low effort but eh + settingsAboutComponent: () => +}); diff --git a/src/equicordplugins/glide/themeDefinitions.tsx b/src/equicordplugins/glide/themeDefinitions.tsx new file mode 100644 index 00000000..bc16de89 --- /dev/null +++ b/src/equicordplugins/glide/themeDefinitions.tsx @@ -0,0 +1,164 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { ThemePreset } from "."; + +export const themes: ThemePreset[] = [ + { + bgcol: "000000", + accentcol: "020202", + textcol: "c0d5e4", + brand: "070707", + name: "Amoled" + }, + { + bgcol: "0e2936", + accentcol: "0c2430", + textcol: "99b0bd", + brand: "124057", + name: "Solar" + }, + { + bgcol: "0e0e36", + accentcol: "0e0c30", + textcol: "bdbfd8", + brand: "171750", + name: "Indigo" + }, + { + bgcol: "8a2b5f", + accentcol: "812658", + textcol: "ffedfb", + brand: "b23982", + name: "Grapefruit" + }, + { + bgcol: "410b05", + accentcol: "360803", + textcol: "f8e6e6", + brand: "681109", + name: "Crimson" + }, + { + bgcol: "184e66", + accentcol: "215a72", + textcol: "d0efff", + brand: "2d718f", + name: "Azure" + }, + { + bgcol: "1d091a", + accentcol: "240d21", + textcol: "f3e1f0", + brand: "411837", + name: "Blackberry" + }, + { + bgcol: "1f073b", + accentcol: "250b44", + textcol: "dfd7e9", + brand: "340d63", + name: "Porple" + }, + { + bgcol: "0a0a0a", + accentcol: "0f0f0f", + textcol: "c9c9c9", + brand: "0a0a0a", + name: "Charcoal" + }, + { + bgcol: "00345b", + accentcol: "002f53", + textcol: "e7d8df", + brand: "944068", + name: "Lofi Pop" + }, + { + bgcol: "471b05", + accentcol: "4e2009", + textcol: "ffffff", + brand: "903e14", + name: "Oaken" + }, + { + bgcol: "040b2b", + accentcol: "000626", + textcol: "ddd0d0", + brand: "040b2b", + name: "Deep Blue" + }, + { + bgcol: "32464a", + accentcol: "2d4145", + textcol: "ddd0d0", + brand: "32464a", + name: "Steel Blue" + }, + { + bgcol: "31031f", + accentcol: "2c001a", + textcol: "ddd0d0", + brand: "31031f", + name: "Velvet" + }, + { + bgcol: "22111f", + accentcol: "1d0c1a", + textcol: "ddd0d0", + brand: "22111f", + name: "Really Oddly Depressed Purple" + }, + { + bgcol: "2b3959", + accentcol: "263454", + textcol: "ddd0d0", + brand: "2b3959", + name: "Light Sky" + }, + { + bgcol: "06403d", + accentcol: "013b38", + textcol: "ddd0d0", + brand: "06403d", + name: "Tealish" + }, + { + bgcol: "273b0b", + accentcol: "223606", + textcol: "ddd0d0", + brand: "273b0b", + name: "Leaf (or a tree perhaps)" + }, + { + bgcol: "1a2022", + accentcol: "151b1d", + textcol: "ddd0d0", + brand: "1a2022", + name: "Steel" + }, + { + bgcol: "1e1e2e", + accentcol: "181825", + textcol: "cdd6f4", + brand: "45475a", + name: "Catppuccin Mocha" + }, + { + bgcol: "303446", + accentcol: "292c3c", + textcol: "c6d0f5", + brand: "414559", + name: "Catppuccin Frappé" + }, + { + bgcol: "6b422e", + accentcol: "754b36", + textcol: "ead9c9", + brand: "8b5032", + name: "Relax" + } +]; diff --git a/src/equicordplugins/googleThat/index.ts b/src/equicordplugins/googleThat/index.ts new file mode 100644 index 00000000..fedf8acf --- /dev/null +++ b/src/equicordplugins/googleThat/index.ts @@ -0,0 +1,77 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { ApplicationCommandOptionType, findOption } from "@api/Commands"; +import { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +function getMessage(opts) { + const inputOption = findOption(opts, "input", ""); + + const queryURL = "" + searchEngines[settings.store.defaultEngine] + encodeURIComponent(inputOption); + + if (settings.store.hyperlink) { + return `[${inputOption}](${queryURL})`; + } + else { + return queryURL; + } +} + +const searchEngines = { + "Google": "https://www.google.com/search?q=", + "Bing": "https://www.bing.com/search?q=", + "Yahoo": "https://search.yahoo.com/search?p=", + "DuckDuckGo": "https://duckduckgo.com/?q=", + "Baidu": "https://www.baidu.com/s?wd=", + "Yandex": "https://yandex.com/search/?text=", + "Ecosia": "https://www.ecosia.org/search?q=", + "Ask": "https://www.ask.com/web?q=", + "LetMeGoogleThatForYou": "https://letmegooglethat.com/?q=" +}; + +const settings = definePluginSettings({ + hyperlink: { + type: OptionType.BOOLEAN, + description: "If the sent link should hyperlink with the query as the label", + default: true + }, + defaultEngine: + { + type: OptionType.SELECT, + description: "The search engine to use", + options: Object.keys(searchEngines).map((key, index) => ({ + label: key, + value: key, + default: index === 0 + })) + } +}); + +export default definePlugin({ + name: "GoogleThat", + description: "Adds a command to send a google search link to a query", + authors: [Devs.Samwich], + tags: ["search", "google", "query", "duckduckgo", "command"], + settings, + commands: [ + { + name: "googlethat", + description: "send a search engine link to a query", + options: [ + { + name: "input", + description: "The search query", + type: ApplicationCommandOptionType.STRING, + required: true, + } + ], + execute: opts => ({ + content: getMessage(opts) + }), + } + ] +}); diff --git a/src/equicordplugins/identity/index.tsx b/src/equicordplugins/identity/index.tsx new file mode 100644 index 00000000..ac301ce3 --- /dev/null +++ b/src/equicordplugins/identity/index.tsx @@ -0,0 +1,148 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { DataStore } from "@api/index"; +import { Flex } from "@components/Flex"; +import { Devs, EquicordDevs } from "@utils/constants"; +import definePlugin, { PluginNative } from "@utils/types"; +import { findComponentByCodeLazy } from "@webpack"; +import { Alerts, Button, FluxDispatcher, Forms, Toasts, UserProfileStore, UserStore } from "@webpack/common"; +const native = VencordNative.pluginHelpers.Identity as PluginNative; + +const CustomizationSection = findComponentByCodeLazy(".customizationSectionBackground"); + +async function SetNewData() { + const PersonData = JSON.parse(await native.RequestRandomUser()); + console.log(PersonData); + + const pfpBase64 = JSON.parse(await native.ToBase64ImageUrl({ imgUrl: PersonData.picture.large })).data; + + // holy moly + FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_AVATAR", avatar: pfpBase64 }); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_GLOBAL_NAME", globalName: `${PersonData.name.first} ${PersonData.name.last}` }); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_PRONOUNS", pronouns: `${PersonData.gender === "male" ? "he/him" : PersonData.gender === "female" ? "she/her" : ""}` }); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_BANNER", banner: null }); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_ACCENT_COLOR", color: null }); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_THEME_COLORS", themeColors: [null, null] }); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_BIO", bio: `Hello! I am ${PersonData.name.first} ${PersonData.name.last}` }); +} + +async function SaveData() { + const userData = UserProfileStore.getUserProfile(UserStore.getCurrentUser().id); + + // the getUserProfile function doesn't return all the information we need, so we append the standard user object data to the end + const extraUserObject: any = { extraUserObject: UserStore.getCurrentUser() }; + + const pfp = JSON.parse(await native.ToBase64ImageUrl({ imgUrl: `https://cdn.discordapp.com/avatars/${userData.userId}/${extraUserObject.extraUserObject.avatar}.webp?size=4096` })).data; + const banner = JSON.parse(await native.ToBase64ImageUrl({ imgUrl: `https://cdn.discordapp.com/banners/${userData.userId}/${userData.banner}.webp?size=4096` })).data; + + const fetchedBase64Data = + { + pfpBase64: pfp, + bannerBase64: banner + }; + + DataStore.set("identity-saved-base", JSON.stringify({ ...userData, ...extraUserObject, ...{ fetchedBase64Data: fetchedBase64Data } })); +} + +async function LoadData() { + const userDataMaybeNull = await DataStore.get("identity-saved-base"); + if (!userDataMaybeNull) { + Toasts.show({ message: "No saved base! Save one first.", id: Toasts.genId(), type: Toasts.Type.FAILURE }); + return; + } + + const userData = JSON.parse(userDataMaybeNull); + + console.log(userData); + + const { pfpBase64, bannerBase64 } = userData.fetchedBase64Data; + + FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_AVATAR", avatar: pfpBase64 }); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_GLOBAL_NAME", globalName: userData.extraUserObject.globalName }); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_PRONOUNS", pronouns: userData.pronouns }); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_BANNER", banner: bannerBase64 }); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_ACCENT_COLOR", color: userData.accentColor }); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_THEME_COLORS", themeColors: userData.themeColors }); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_BIO", bio: userData.bio }); +} + +function ResetCard() { + return ( + + + + + + + + ); +} + +export default definePlugin({ + name: "Identity", + description: "Allows you to edit your profile to a random fake person with the click of a button", + authors: [Devs.Samwich, EquicordDevs.port22exposed], + ResetCard: ResetCard, + patches: [ + { + find: "DefaultCustomizationSections", + replacement: { + match: /(?<=#{intl::USER_SETTINGS_AVATAR_DECORATION}\)},"decoration"\),)/, + replace: "$self.ResetCard()," + } + }, + ] +}); diff --git a/src/equicordplugins/identity/native.ts b/src/equicordplugins/identity/native.ts new file mode 100644 index 00000000..b838900c --- /dev/null +++ b/src/equicordplugins/identity/native.ts @@ -0,0 +1,29 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export async function RequestRandomUser() { + const data = await fetch("https://randomuser.me/api").then(e => e.json()); + + return JSON.stringify(data.results[0]); +} + +export async function ToBase64ImageUrl(_, data) { + const { imgUrl } = data; + + try { + const fetchImageUrl = await fetch(imgUrl); + const responseArrBuffer = await fetchImageUrl.arrayBuffer(); + + const toBase64 = + `data:${fetchImageUrl.headers.get("Content-Type") || "image/png"};base64,${Buffer.from(responseArrBuffer).toString("base64")}`; + + return JSON.stringify({ data: toBase64 }); + + } catch (error) { + console.error("Error converting image to Base64:", error); + return JSON.stringify({ error: "Failed to convert image to Base64" }); + } +} diff --git a/src/equicordplugins/moreStickers/components/misc.tsx b/src/equicordplugins/moreStickers/components/misc.tsx index 2d760c0b..d3a35983 100644 --- a/src/equicordplugins/moreStickers/components/misc.tsx +++ b/src/equicordplugins/moreStickers/components/misc.tsx @@ -5,12 +5,13 @@ */ import * as DataStore from "@api/DataStore"; +import { CheckedTextInput } from "@components/CheckedTextInput"; import { Flex } from "@components/Flex"; import { Button, Forms, React, TabBar, Text, TextArea, Toasts } from "@webpack/common"; import { JSX } from "react"; -import { convert as convertLineEP, isLineEmojiPackHtml, parseHtml as getLineEPFromHtml } from "../lineEmojis"; -import { convert as convertLineSP, isLineStickerPackHtml, parseHtml as getLineSPFromHtml } from "../lineStickers"; +import { convert as convertLineEP, getIdFromUrl as getLineEmojiPackIdFromUrl, getStickerPackById as getLineEmojiPackById, isLineEmojiPackHtml, parseHtml as getLineEPFromHtml } from "../lineEmojis"; +import { convert as convertLineSP, getIdFromUrl as getLineStickerPackIdFromUrl, getStickerPackById as getLineStickerPackById, isLineStickerPackHtml, parseHtml as getLineSPFromHtml } from "../lineStickers"; import { isV1, migrate } from "../migrate-v1"; import { deleteStickerPack, getStickerPack, getStickerPackMetas, saveStickerPack } from "../stickers"; import { SettingsTabsKey, Sticker, StickerPack, StickerPackMeta } from "../types"; @@ -88,8 +89,9 @@ const StickerPackMetadata = ({ meta, hoveredStickerPackId, setHoveredStickerPack export const Settings = () => { const [stickerPackMetas, setstickerPackMetas] = React.useState([]); + const [addStickerUrl, setAddStickerUrl] = React.useState(""); const [addStickerHtml, setAddStickerHtml] = React.useState(""); - const [tab, setTab] = React.useState(SettingsTabsKey.ADD_STICKER_PACK_HTML); + const [tab, setTab] = React.useState(SettingsTabsKey.ADD_STICKER_PACK_URL); const [hoveredStickerPackId, setHoveredStickerPackId] = React.useState(null); const [_isV1, setV1] = React.useState(false); @@ -121,6 +123,114 @@ export const Settings = () => { } + {tab === SettingsTabsKey.ADD_STICKER_PACK_URL && +
+ Add Sticker Pack from URL + +

+ Currently LINE stickers/emojis supported only.
+ + Get Telegram stickers with VencordNative.native.openExternal("https://github.com/lekoOwO/MoreStickersConverter")}> MoreStickersConverter. +

+
+ + + { + try { + getLineStickerPackIdFromUrl(v); + return true; + } catch (e: any) { } + try { + getLineEmojiPackIdFromUrl(v); + return true; + } catch (e: any) { } + + return "Invalid URL"; + }} + placeholder="Sticker Pack URL" + /> + + + +
+ } {tab === SettingsTabsKey.ADD_STICKER_PACK_HTML &&
Add Sticker Pack from HTML diff --git a/src/equicordplugins/moreStickers/lineEmojis.ts b/src/equicordplugins/moreStickers/lineEmojis.ts index f8bb681d..39c0e4d3 100644 --- a/src/equicordplugins/moreStickers/lineEmojis.ts +++ b/src/equicordplugins/moreStickers/lineEmojis.ts @@ -5,6 +5,7 @@ */ import { LineEmoji, LineEmojiPack, Sticker, StickerPack } from "./types"; +import { corsFetch } from "./utils"; export interface StickerCategory { title: string; @@ -122,3 +123,16 @@ export function parseHtml(html: string): LineEmojiPack { export function isLineEmojiPackHtml(html: string): boolean { return html.includes("data-test=\"emoji-name-title\""); } + +/** + * Get stickers from LINE + * + * @param {string} id The id of the sticker pack. + * @return {Promise} The sticker pack. + */ +export async function getStickerPackById(id: string, region = "en"): Promise { + const res = await corsFetch(`https://store.line.me/emojishop/product/${id}/${region}`); + const html = await res.text(); + + return parseHtml(html); +} diff --git a/src/equicordplugins/moreStickers/lineStickers.ts b/src/equicordplugins/moreStickers/lineStickers.ts index 9b6a58d8..66bb7c71 100644 --- a/src/equicordplugins/moreStickers/lineStickers.ts +++ b/src/equicordplugins/moreStickers/lineStickers.ts @@ -5,6 +5,7 @@ */ import { LineSticker, LineStickerPack, Sticker, StickerPack } from "./types"; +import { corsFetch } from "./utils"; export interface StickerCategory { title: string; @@ -122,3 +123,16 @@ export function parseHtml(html: string): LineStickerPack { export function isLineStickerPackHtml(html: string): boolean { return html.includes("data-test=\"sticker-name-title\""); } + +/** + * Get stickers from LINE + * + * @param {string} id The id of the sticker pack. + * @return {Promise} The sticker pack. + */ +export async function getStickerPackById(id: string, region = "en"): Promise { + const res = await corsFetch(`https://store.line.me/stickershop/product/${id}/${region}`); + const html = await res.text(); + + return parseHtml(html); +} diff --git a/src/equicordplugins/moreStickers/testdata.ts b/src/equicordplugins/moreStickers/testdata.ts new file mode 100644 index 00000000..9a6f4f70 --- /dev/null +++ b/src/equicordplugins/moreStickers/testdata.ts @@ -0,0 +1,77 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { setRecentStickers } from "./components"; +import { + convert, + getStickerPackById +} from "./lineStickers"; +import { + deleteStickerPack, + getStickerPackMetas, + saveStickerPack +} from "./stickers"; +import { StickerPack } from "./types"; + +export async function initTest() { + console.log("initTest."); + + console.log("Clearing recent stickers."); + setRecentStickers([]); + + // Clear all sticker packs + console.log("Clearing all sticker packs."); + const stickerPackMetas = await getStickerPackMetas(); + for (const meta of stickerPackMetas) { + await deleteStickerPack(meta.id); + } + + // Add test sticker packs + console.log("Adding test sticker packs."); + const lineStickerPackIds = [ + "22814489", // LV.47 + "22567773", // LV.46 + "22256215", // LV.45 + "21936635", // LV.44 + "21836565", // LV.43 + ]; + const ps: Promise[] = []; + for (const id of lineStickerPackIds) { + ps.push((async () => { + try { + const lsp = await getStickerPackById(id); + const sp = convert(lsp); + return sp; + } catch (e) { + console.error("Failed to fetch sticker pack: " + id); + console.error(e); + return null; + } + })()); + } + const stickerPacks = (await Promise.all(ps)).filter(sp => sp !== null) as StickerPack[]; + + console.log("Saving test sticker packs."); + for (const sp of stickerPacks) { + await saveStickerPack(sp); + } + + console.log(await getStickerPackMetas()); +} + +export async function clearTest() { + console.log("clearTest."); + + console.log("Clearing recent stickers."); + setRecentStickers([]); + + // Clear all sticker packs + console.log("Clearing all sticker packs."); + const stickerPackMetas = await getStickerPackMetas(); + for (const meta of stickerPackMetas) { + await deleteStickerPack(meta.id); + } +} diff --git a/src/equicordplugins/moreStickers/types.ts b/src/equicordplugins/moreStickers/types.ts index e675f859..5d545bdf 100644 --- a/src/equicordplugins/moreStickers/types.ts +++ b/src/equicordplugins/moreStickers/types.ts @@ -97,6 +97,7 @@ export interface PickerContentRowGrid { } export enum SettingsTabsKey { + ADD_STICKER_PACK_URL = "Add from URL", ADD_STICKER_PACK_HTML = "Add from HTML", ADD_STICKER_PACK_FILE = "Add from File", MISC = "Misc", diff --git a/src/equicordplugins/moreStickers/utils.tsx b/src/equicordplugins/moreStickers/utils.tsx index e2e4b707..f73a5e4c 100644 --- a/src/equicordplugins/moreStickers/utils.tsx +++ b/src/equicordplugins/moreStickers/utils.tsx @@ -14,6 +14,16 @@ import { FFmpegState } from "./types"; export const cl = classNameFactory("vc-more-stickers-"); export const clPicker = (className: string, ...args: any[]) => cl("picker-" + className, ...args); +const CORS_PROXY = "https://corsproxy.io?"; + +function corsUrl(url: string | URL) { + return CORS_PROXY + encodeURIComponent(url.toString()); +} + +export function corsFetch(url: string | URL, init?: RequestInit | undefined) { + return fetch(corsUrl(url), init); +} + export class Mutex { current = Promise.resolve(); lock() { diff --git a/src/equicordplugins/sekaiStickers/Components/Canvas.tsx b/src/equicordplugins/sekaiStickers/Components/Canvas.tsx new file mode 100644 index 00000000..c795290c --- /dev/null +++ b/src/equicordplugins/sekaiStickers/Components/Canvas.tsx @@ -0,0 +1,27 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { useEffect, useRef } from "@webpack/common"; + +const Canvas = props => { + + const { draw, ...rest } = props; + const canvasRef = useRef(null); + + useEffect(() => { + + const canvas = canvasRef.current; + // @ts-ignore + const context = canvas.getContext("2d"); + + draw(context); + + }, [draw]); + + return ; +}; + +export default Canvas; diff --git a/src/equicordplugins/sekaiStickers/Components/Picker.tsx b/src/equicordplugins/sekaiStickers/Components/Picker.tsx new file mode 100644 index 00000000..7e0254e2 --- /dev/null +++ b/src/equicordplugins/sekaiStickers/Components/Picker.tsx @@ -0,0 +1,51 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Flex } from "@components/Flex"; +import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal"; +import { React, ScrollerThin, Text, TextInput } from "@webpack/common"; + +import { characters } from "../characters.json"; + +export default function CharSelectModal({ modalProps, setCharacter }: { modalProps: ModalProps; setCharacter?: any; }) { + const [search, setSearch] = React.useState(""); + + const memoedSearchChar = React.useMemo(() => { + const s = search.toLowerCase(); + return characters.map((c, index) => { + if ( + s === c.id || + c.name.toLowerCase().includes(s) || + c.character.toLowerCase().includes(s) + ) { + return ( + { modalProps.onClose(); setCharacter(index); }} src={`https://st.ayaka.one/img/${c.img}`} srcSet={`https://st.ayaka.one/img/${c.img}`} loading="lazy" /> + ); + } + + return null; + }); + }, [search, characters]); + return ( + + + Select character menu + + + + + setSearch(e)} /> + +
+ {memoedSearchChar} +
+
+
+ +
+
+ ); +} diff --git a/src/equicordplugins/sekaiStickers/Components/SekaiStickersModal.tsx b/src/equicordplugins/sekaiStickers/Components/SekaiStickersModal.tsx new file mode 100644 index 00000000..4f283647 --- /dev/null +++ b/src/equicordplugins/sekaiStickers/Components/SekaiStickersModal.tsx @@ -0,0 +1,138 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Flex } from "@components/Flex"; +import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { Button, ChannelStore, Forms, React, SelectedChannelStore, Slider, Switch, Text, TextArea, UploadHandler } from "@webpack/common"; + +import { characters } from "../characters.json"; +import Canvas from "./Canvas"; +import CharSelectModal from "./Picker"; + +export default function SekaiStickersModal({ modalProps, settings }: { modalProps: ModalProps; settings: any; }) { + const [text, setText] = React.useState("奏でーかわいい"); + const [character, setChracter] = React.useState(49); + const [fontSize, setFontSize] = React.useState(characters[character].defaultText.s); + const [rotate, setRotate] = React.useState(characters[character].defaultText.r); + const [curve, setCurve] = React.useState(false); + const [isImgLoaded, setImgLoaded] = React.useState(false); + const [position, setPosition] = React.useState<{ x: number, y: number; }>({ x: characters[character].defaultText.x, y: characters[character].defaultText.y }); + const [spaceSize, setSpaceSize] = React.useState(1); + let canvast!: HTMLCanvasElement; + const img = new Image(); + img.crossOrigin = "anonymous"; + img.src = "https://st.ayaka.one/img/" + characters[character].img; + + React.useEffect(() => { + setPosition({ + x: characters[character].defaultText.x, + y: characters[character].defaultText.y + }); + setFontSize(characters[character].defaultText.s); + setRotate(characters[character].defaultText.r); + setImgLoaded(false); + }, [character]); + + img.onload = () => { setImgLoaded(true); }; + const angle = (Math.PI * text.length) / 7; + + const draw = ctx => { + ctx.canvas.width = 296; + ctx.canvas.height = 256; + + if (isImgLoaded && document.fonts.check("12px YurukaStd")) { + const hRatio = ctx.canvas.width / img.width; + const vRatio = ctx.canvas.height / img.height; + const ratio = Math.min(hRatio, vRatio); + const centerShiftX = (ctx.canvas.width - img.width * ratio) / 2; + const centerShiftY = (ctx.canvas.height - img.height * ratio) / 2; + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + ctx.drawImage( + img, + 0, + 0, + img.width, + img.height, + centerShiftX, + centerShiftY, + img.width * ratio, + img.height * ratio + ); + ctx.font = `${fontSize}px YurukaStd, SSFangTangTi`; + ctx.lineWidth = 9; + ctx.save(); + + ctx.translate(position.x, position.y); + ctx.rotate(rotate / 10); + ctx.textAlign = "center"; + ctx.strokeStyle = "white"; + ctx.fillStyle = characters[character].color; + const lines = text.split("\n"); + if (curve) { + for (const line of lines) { + for (let i = 0; i < line.length; i++) { + ctx.rotate(angle / line.length / 2.5); + ctx.save(); + ctx.translate(0, -1 * fontSize * 3.5); + ctx.strokeText(line[i], 0, -1 * spaceSize); + ctx.fillText(line[i], 0, -1 * spaceSize); + ctx.restore(); + } + } + } else { + for (let i = 0, k = 0; i < lines.length; i++) { + ctx.strokeText(lines[i], 0, k); + ctx.fillText(lines[i], 0, k); + k += spaceSize; + } + ctx.restore(); + } + canvast = ctx.canvas; + } + }; + return ( + + + Sekai Stickers + + + + +
+ + Text Y Pos + { va = Math.round(va); setPosition({ x: position.x, y: curve ? 256 + fontSize * 3 - va : 256 - va }); }} initialValue={curve ? 256 - position.y + fontSize * 3 : 256 - position.y} orientation={"vertical"} onValueRender={va => String(Math.round(va))} /> + Text XZ Pos + { va = Math.round(va); setPosition({ y: position.y, x: va }); }} initialValue={position.x} orientation={"horizontal"} onValueRender={(v: number) => String(Math.round(v))} /> +
+
+ Text +