Add All Those Plugins Back

This commit is contained in:
thororen1234 2025-04-09 08:23:40 -04:00
parent 0eafb62a6b
commit 1d99644de7
No known key found for this signature in database
40 changed files with 8824 additions and 57 deletions

View file

@ -11,7 +11,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
### Extra included plugins
<details>
<summary>152 additional plugins</summary>
<summary>163 additional plugins</summary>
### 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

View file

@ -318,12 +318,6 @@ function ThemesTab() {
<Forms.FormText>If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder.</Forms.FormText>
</Card>
<Card className="vc-settings-card">
<Forms.FormTitle tag="h5">External Resources</Forms.FormTitle>
<Forms.FormText>For security reasons, loading resources (styles, fonts, images, ...) from most sites is blocked.</Forms.FormText>
<Forms.FormText>Make sure all your assets are hosted on GitHub, GitLab, Codeberg, Imgur, Discord or Google Fonts.</Forms.FormText>
</Card>
<Forms.FormSection title="Local Themes">
<QuickActionCard>
<>

View file

@ -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();
},
});

View file

@ -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;
}

View file

@ -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),
};
},
}]
});

View file

@ -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<string> {
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()
})
}]
});

View file

@ -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<string> {
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()
})
}]
});

View file

@ -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<typeof import("./native")>;
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 <>
<Forms.FormText>
Discord rich presence for Gensokyo Radio!
</Forms.FormText>
</>;
},
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<Activity | null> {
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,
};
}
});

View file

@ -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<TrackData | null> {
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",
};
}

View file

@ -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<string>;
name: string;
application_id: string;
metadata?: {
button_urls?: Array<string>;
};
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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

View file

@ -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;
}

View file

@ -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 (
<>
<Forms.FormSection>
<Forms.FormTitle>{"Preset name"}</Forms.FormTitle>
<TextInput
type="text"
value={inputtedName}
onChange={setInputtedName}
placeholder={"Enter a name"}
/>
</Forms.FormSection>
<Button onClick={() => {
copyPreset(inputtedName);
}}>Copy preset</Button>
<Button onClick={() => {
generateAndApplyProceduralTheme();
}}>Generate Random</Button>
</>
);
}
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: () => <ColorPick propertyname="Primary" />
},
Accent: {
type: OptionType.COMPONENT,
description: "",
default: "313338",
component: () => <ColorPick propertyname="Accent" />
},
Text: {
type: OptionType.COMPONENT,
description: "",
default: "ffffff",
component: () => <ColorPick propertyname="Text" />
},
Brand: {
type: OptionType.COMPONENT,
description: "",
default: "ffffff",
component: () => <ColorPick propertyname="Brand" />
},
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: () => <CopyPresetComponent />
},
ExportTheme:
{
type: OptionType.COMPONENT,
description: "",
default: "",
component: () => <Button onClick={() => {
copyCSS();
Toasts.show({
id: Toasts.genId(),
message: "Successfully copied theme!",
type: Toasts.Type.SUCCESS
});
}} >Copy The CSS for your current configuration.</Button>
}
});
export function ColorPick({ propertyname }: { propertyname: string; }) {
return (
<div className="color-options-container">
<Forms.FormTitle tag="h3">{propertyname}</Forms.FormTitle>
<ColorPicker
color={parseInt(settings.store[propertyname], 16)}
onChange={color => {
const hexColor = color.toString(16).padStart(6, "0");
settings.store[propertyname] = hexColor;
injectCSS();
}
}
showEyeDropper={false}
/>
</div>
);
}
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: () => <img src="https://cdn.nest.rip/uploads/97fdf6c1-764c-4445-9422-d3d52af7434c.webp" style={{ width: "568px", borderRadius: "30px" }} alt=""></img>
});

View file

@ -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"
}
];

View file

@ -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)
}),
}
]
});

View file

@ -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<typeof import("./native")>;
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 (
<CustomizationSection
title={"Identity"}
hasBackground={true}
hideDivider={false}
>
<Flex>
<Button
onClick={() => {
Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>
Saving your base profile will allow you to have a backup of your actual profile
</Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
If you save, it will overwrite your previous data.
</Forms.FormText>
</div>,
confirmText: "Save Anyway",
cancelText: "Cancel",
onConfirm: SaveData
});
}}
size={Button.Sizes.MEDIUM}
>
Save Base
</Button>
<Button
onClick={() => {
Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>
Loading your base profile will restore your actual profile settings
</Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
If you load, it will overwrite your current profile configuration.
</Forms.FormText>
</div>,
confirmText: "Load Anyway",
cancelText: "Cancel",
onConfirm: LoadData
});
}}
size={Button.Sizes.MEDIUM}
>
Load Base
</Button>
<Button
onClick={SetNewData}
size={Button.Sizes.MEDIUM}
>
Randomise
</Button>
</Flex>
</CustomizationSection>
);
}
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(),"
}
},
]
});

View file

@ -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" });
}
}

View file

@ -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<StickerPackMeta[]>([]);
const [addStickerUrl, setAddStickerUrl] = React.useState<string>("");
const [addStickerHtml, setAddStickerHtml] = React.useState<string>("");
const [tab, setTab] = React.useState<SettingsTabsKey>(SettingsTabsKey.ADD_STICKER_PACK_HTML);
const [tab, setTab] = React.useState<SettingsTabsKey>(SettingsTabsKey.ADD_STICKER_PACK_URL);
const [hoveredStickerPackId, setHoveredStickerPackId] = React.useState<string | null>(null);
const [_isV1, setV1] = React.useState<boolean>(false);
@ -121,6 +123,114 @@ export const Settings = () => {
}
</TabBar>
{tab === SettingsTabsKey.ADD_STICKER_PACK_URL &&
<div className="section">
<Forms.FormTitle tag="h5">Add Sticker Pack from URL</Forms.FormTitle>
<Forms.FormText>
<p>
Currently LINE stickers/emojis supported only. <br />
Get Telegram stickers with <a href="#" onClick={() => VencordNative.native.openExternal("https://github.com/lekoOwO/MoreStickersConverter")}> MoreStickersConverter</a>.
</p>
</Forms.FormText>
<Flex flexDirection="row" style={{
alignItems: "center",
justifyContent: "center"
}} >
<span style={{
flexGrow: 1
}}>
<CheckedTextInput
value={addStickerUrl}
onChange={setAddStickerUrl}
validate={(v: string) => {
try {
getLineStickerPackIdFromUrl(v);
return true;
} catch (e: any) { }
try {
getLineEmojiPackIdFromUrl(v);
return true;
} catch (e: any) { }
return "Invalid URL";
}}
placeholder="Sticker Pack URL"
/>
</span>
<Button
size={Button.Sizes.SMALL}
onClick={async e => {
e.preventDefault();
let type: string = "";
try {
getLineStickerPackIdFromUrl(addStickerUrl);
type = "LineStickerPack";
} catch (e: any) { }
try {
getLineEmojiPackIdFromUrl(addStickerUrl);
type = "LineEmojiPack";
} catch (e: any) { }
let errorMessage = "";
switch (type) {
case "LineStickerPack": {
try {
const id = getLineStickerPackIdFromUrl(addStickerUrl);
const lineSP = await getLineStickerPackById(id);
const stickerPack = convertLineSP(lineSP);
await saveStickerPack(stickerPack);
} catch (e: any) {
console.error(e);
errorMessage = e.message;
}
break;
}
case "LineEmojiPack": {
try {
const id = getLineEmojiPackIdFromUrl(addStickerUrl);
const lineEP = await getLineEmojiPackById(id);
const stickerPack = convertLineEP(lineEP);
await saveStickerPack(stickerPack);
} catch (e: any) {
console.error(e);
errorMessage = e.message;
}
break;
}
}
setAddStickerUrl("");
refreshStickerPackMetas();
if (errorMessage) {
Toasts.show({
message: errorMessage,
type: Toasts.Type.FAILURE,
id: Toasts.genId(),
options: {
duration: 1000
}
});
} else {
Toasts.show({
message: "Sticker Pack added",
type: Toasts.Type.SUCCESS,
id: Toasts.genId(),
options: {
duration: 1000
}
});
}
}}
>Insert</Button>
</Flex>
</div>
}
{tab === SettingsTabsKey.ADD_STICKER_PACK_HTML &&
<div className="section">
<Forms.FormTitle tag="h5">Add Sticker Pack from HTML</Forms.FormTitle>

View file

@ -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<LineEmojiPack>} The sticker pack.
*/
export async function getStickerPackById(id: string, region = "en"): Promise<LineEmojiPack> {
const res = await corsFetch(`https://store.line.me/emojishop/product/${id}/${region}`);
const html = await res.text();
return parseHtml(html);
}

View file

@ -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<LineStickerPack>} The sticker pack.
*/
export async function getStickerPackById(id: string, region = "en"): Promise<LineStickerPack> {
const res = await corsFetch(`https://store.line.me/stickershop/product/${id}/${region}`);
const html = await res.text();
return parseHtml(html);
}

View file

@ -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<StickerPack | null>[] = [];
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);
}
}

View file

@ -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",

View file

@ -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() {

View file

@ -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 <canvas ref={canvasRef} {...rest} />;
};
export default Canvas;

View file

@ -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<string>("");
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 (
<img key={index} onClick={() => { 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 (
<ModalRoot {...modalProps} size={ModalSize.DYNAMIC}>
<ModalHeader>
<Text variant="heading-lg/bold" style={{ flexGrow: 1 }}>Select character menu</Text>
<ModalCloseButton onClick={modalProps.onClose} ></ModalCloseButton>
</ModalHeader>
<ModalContent>
<Flex flexDirection="column" style={{ paddingTop: 12 }}>
<TextInput content="mafuyu" placeholder="Mafuyu" onChange={(e: string) => setSearch(e)} />
<ScrollerThin style={{ height: 520 }}>
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 330px)", rowGap: 6, columnGap: 5, gridTemplateRows: "repeat(3, 256px)" }}>
{memoedSearchChar}
</div>
</ScrollerThin>
</Flex>
</ModalContent>
</ModalRoot>
);
}

View file

@ -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<string>("奏でーかわいい");
const [character, setChracter] = React.useState<number>(49);
const [fontSize, setFontSize] = React.useState<number>(characters[character].defaultText.s);
const [rotate, setRotate] = React.useState<number>(characters[character].defaultText.r);
const [curve, setCurve] = React.useState<boolean>(false);
const [isImgLoaded, setImgLoaded] = React.useState<boolean>(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<number>(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 (
<ModalRoot {...modalProps} size={ModalSize.DYNAMIC}>
<ModalHeader>
<Text variant="heading-lg/bold" style={{ flexGrow: 1 }}>Sekai Stickers</Text>
<ModalCloseButton onClick={modalProps.onClose} ></ModalCloseButton>
</ModalHeader>
<ModalContent>
<Flex flexDirection="row" style={{ paddingTop: 12 }}>
<div style={{ marginRight: 30 }}>
<Canvas draw={draw} id="SekaiCard_Canvas" />
<Forms.FormTitle>Text Y Pos</Forms.FormTitle>
<Slider minValue={0} maxValue={256} asValueChanges={va => { 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))} />
<Forms.FormTitle>Text XZ Pos</Forms.FormTitle>
<Slider minValue={0} maxValue={296} asValueChanges={va => { va = Math.round(va); setPosition({ y: position.y, x: va }); }} initialValue={position.x} orientation={"horizontal"} onValueRender={(v: number) => String(Math.round(v))} />
</div>
<div style={{ marginRight: 10, width: "30vw" }}>
<Forms.FormTitle>Text</Forms.FormTitle>
<TextArea onChange={setText} placeholder={text} rows={4} spellCheck={false} />
<Forms.FormTitle>Rotation</Forms.FormTitle>
<Slider markers={[-10, -5, 0, 5, 10]} stickToMarkers={false} minValue={-10} maxValue={10} asValueChanges={val => setRotate(val)} initialValue={rotate} keyboardStep={0.2} orientation={"horizontal"} onValueRender={(v: number) => String(v.toFixed(2))} />
<Forms.FormTitle>Font Size</Forms.FormTitle>
<Slider minValue={10} asValueChanges={val => setFontSize(Math.round(val))} maxValue={100} initialValue={fontSize} keyboardStep={1} orientation={"horizontal"} onValueRender={(v: number) => String(Math.round(v))} />
<Forms.FormTitle>Spacing</Forms.FormTitle>
<Slider markers={[18, 36, 72, 100]} stickToMarkers={false} minValue={18} maxValue={100} initialValue={spaceSize} asValueChanges={e => setSpaceSize(e)} onValueRender={e => String(Math.round(e))} />
<Switch value={curve} onChange={val => setCurve(val)}>Enable curve</Switch>
<Button onClick={() => openModal(props => <CharSelectModal modalProps={props} setCharacter={setChracter} />)}>Switch Character</Button>
</div>
</Flex>
</ModalContent>
<ModalFooter>
<Flex flexDirection="row" style={{ gap: 12 }}>
<Button onClick={() => {
if (settings.store.AutoCloseModal) modalProps.onClose();
canvast.toBlob(blob => {
const file = new File([blob as Blob], `${characters[character].character}-sekai_cards.png`, { type: "image/png" });
UploadHandler.promptToUpload([file], ChannelStore.getChannel(SelectedChannelStore.getChannelId()), 0);
});
}}>Upload as Attachment</Button>
</Flex>
</ModalFooter>
</ModalRoot>
);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addChatBarButton, ChatBarButton, ChatBarButtonFactory, removeChatBarButton } from "@api/ChatButtons";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import SekaiStickersModal from "./Components/SekaiStickersModal";
import { kanadeSvg } from "./kanade.svg";
const settings = definePluginSettings({
AutoCloseModal: {
type: OptionType.BOOLEAN,
description: "Auto close modal when done",
default: true
}
});
const SekaiStickerChatButton: ChatBarButtonFactory = () => {
return (
<ChatBarButton onClick={() => openModal(props => <SekaiStickersModal modalProps={props} settings={settings} />)} tooltip="Sekai Stickers">
{kanadeSvg()}
</ChatBarButton>
);
};
let IS_FONTS_LOADED = false;
export default definePlugin({
name: "SekaiStickers",
description: "Sekai Stickers built in discord originally from github.com/TheOriginalAyaka",
authors: [Devs.MaiKokain],
dependencies: ["ChatInputButtonAPI"],
settings,
start: async () => {
const fonts = [{ name: "YurukaStd", url: "https://raw.githubusercontent.com/TheOriginalAyaka/sekai-stickers/47a2ca33b8cb35f59800e8faad48980e4ce5ea71/src/fonts/YurukaStd.woff2" }, { name: "SSFangTangTi", url: "https://raw.githubusercontent.com/TheOriginalAyaka/sekai-stickers/main/src/fonts/ShangShouFangTangTi.woff2" }];
if (!IS_FONTS_LOADED) {
fonts.map(n => {
new FontFace(n.name, `url(${n.url})`).load().then(
font => { document.fonts.add(font); },
err => { console.log(err); }
);
});
IS_FONTS_LOADED = true;
}
addChatBarButton("SekaiStickers", SekaiStickerChatButton);
},
stop: () => removeChatBarButton("SekaiStickers")
});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,219 @@
/*
* 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";
import { ApplicationAssetUtils, FluxDispatcher } from "@webpack/common";
import { Activity, ActivityType, BanchoStatusEnum, GameState, Modes, TosuApi, UserLoginStatus } from "./type";
const socketId = "tosu";
const OSU_APP_ID = "367827983903490050";
const OSU_LARGE_IMAGE = "373344233077211136";
const OSU_STARDARD_SMALL_IMAGE = "373370493127884800";
const OSU_MANIA_SMALL_IMAGE = "373370588703621136";
const OSU_TAIKO_SMALL_IMAGE = "373370519891738624";
const OSU_CATCH_SMALL_IMAGE = "373370543161999361";
const throttledOnMessage = throttle(onMessage, 3000, () => FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null, socketId: "tosu" }));
let ws: WebSocket;
let wsReconnect: NodeJS.Timeout;
export default definePlugin({
name: "TosuRPC",
description: "osu! RPC with data from tosu",
authors: [Devs.AutumnVN],
start() {
(function connect() {
ws = new WebSocket("ws://localhost:24050/websocket/v2");
ws.addEventListener("error", () => ws.close());
ws.addEventListener("close", () => wsReconnect = setTimeout(connect, 5000));
ws.addEventListener("message", ({ data }) => throttledOnMessage(data));
})();
},
stop() {
ws.close();
clearTimeout(wsReconnect);
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null, socketId: "tosu" });
}
});
async function onMessage(data: string) {
const json: TosuApi = JSON.parse(data);
// @ts-ignore
if (json.error) return FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null, socketId: "tosu" });
const { state, session, profile, beatmap, play, resultsScreen } = json;
const activity: Activity = {
application_id: OSU_APP_ID,
name: "osu!",
type: ActivityType.PLAYING,
assets: {
large_image: OSU_LARGE_IMAGE,
large_text: profile.userStatus.number === UserLoginStatus.Connected ? `${profile.name} | #${profile.globalRank} | ${Math.round(profile.pp)}pp` : undefined,
},
timestamps: {
start: Date.now() - session.playTime
},
flags: 1 << 0,
};
switch (profile.mode.number) {
case Modes.Osu:
activity.assets.small_image = OSU_STARDARD_SMALL_IMAGE;
activity.assets.small_text = "osu!";
break;
case Modes.Mania:
activity.assets.small_image = OSU_MANIA_SMALL_IMAGE;
activity.assets.small_text = "osu!mania";
break;
case Modes.Taiko:
activity.assets.small_image = OSU_TAIKO_SMALL_IMAGE;
activity.assets.small_text = "osu!taiko";
break;
case Modes.Fruits:
activity.assets.small_image = OSU_CATCH_SMALL_IMAGE;
activity.assets.small_text = "osu!catch";
break;
}
let player = "";
let mods = "";
let fc = "";
let combo = "";
let h100 = "";
let h50 = "";
let h0 = "";
let sb = "";
let pp = "";
switch (state.number) {
case GameState.Play:
activity.type = profile.banchoStatus.number === BanchoStatusEnum.Playing ? ActivityType.PLAYING : ActivityType.WATCHING;
player = profile.banchoStatus.number === BanchoStatusEnum.Playing ? "" : `${play.playerName} | `;
mods = play.mods.name ? `+${play.mods.name} ` : "";
activity.name = `${player}${beatmap.artist} - ${beatmap.title} [${beatmap.version}] ${mods}(${beatmap.mapper}, ${beatmap.stats.stars.total.toFixed(2)}*)`;
combo = play.hits[0] === 0 && play.hits.sliderBreaks === 0
? `${play.combo.current}x`
: `${play.combo.current}x/${play.combo.max}x`;
pp = play.hits[0] === 0 && play.hits.sliderBreaks === 0
? `${Math.round(play.pp.current)}pp`
: `${Math.round(play.pp.current)}pp/${Math.round(play.pp.fc)}pp`;
activity.details = `${play.accuracy.toFixed(2)}% | ${combo} | ${pp}`;
h100 = play.hits[100] > 0 ? `${play.hits[100]}x100` : "";
h50 = play.hits[50] > 0 ? `${play.hits[50]}x50` : "";
h0 = play.hits[0] > 0 ? `${play.hits[0]}xMiss` : "";
sb = play.hits.sliderBreaks > 0 ? `${play.hits.sliderBreaks}xSB` : "";
activity.state = [h100, h50, h0, sb].filter(Boolean).join(" | ");
const playRank = await getAsset(`https://raw.githubusercontent.com/AutumnVN/gosu-rich-presence/main/grade/${play.rank.current.toLowerCase().replace("x", "ss")}.png`);
activity.assets.small_image = playRank;
activity.assets.small_text = undefined;
break;
case GameState.ResultScreen:
activity.type = ActivityType.WATCHING;
mods = resultsScreen.mods.name ? `+${resultsScreen.mods.name} ` : "";
activity.name = `${resultsScreen.playerName} | ${beatmap.artist} - ${beatmap.title} [${beatmap.version}] ${mods}(${beatmap.mapper}, ${beatmap.stats.stars.total.toFixed(2)}*)`;
fc = resultsScreen.maxCombo === beatmap.stats.maxCombo ? "FC" : `| ${resultsScreen.maxCombo}x/${beatmap.stats.maxCombo}x`;
pp = !resultsScreen.pp.current ? ""
: Math.round(resultsScreen.pp.current) === Math.round(resultsScreen.pp.fc)
? `| ${Math.round(resultsScreen.pp.current)}pp`
: `| ${Math.round(resultsScreen.pp.current)}pp/${Math.round(resultsScreen.pp.fc)}pp`;
activity.details = `${resultsScreen.accuracy.toFixed(2)}% ${fc} ${pp}`;
h100 = resultsScreen.hits[100] > 0 ? `${resultsScreen.hits[100]}x100` : "";
h50 = resultsScreen.hits[50] > 0 ? `${resultsScreen.hits[50]}x50` : "";
h0 = resultsScreen.hits[0] > 0 ? `${resultsScreen.hits[0]}xMiss` : "";
sb = play.hits.sliderBreaks > 0 ? `${play.hits.sliderBreaks}xSB` : "";
activity.state = [h100, h50, h0].filter(Boolean).join(" | ");
const resultRank = await getAsset(`https://raw.githubusercontent.com/AutumnVN/gosu-rich-presence/main/grade/${resultsScreen.rank.toLowerCase().replace("x", "ss")}.png`);
activity.assets.small_image = resultRank;
activity.assets.small_text = undefined;
break;
default:
activity.type = ActivityType.LISTENING;
mods = play.mods.name ? `+${play.mods.name} ` : "";
activity.name = `${beatmap.artist} - ${beatmap.title} [${beatmap.version}] ${mods}(${beatmap.mapper}, ${beatmap.stats.stars.total.toFixed(2)}*)`;
switch (state.number) {
case GameState.Menu: activity.details = "Main Menu"; break;
case GameState.Edit: activity.details = "Edit"; break;
case GameState.SelectEdit: activity.details = "Song Select (Edit)"; break;
case GameState.SelectPlay: activity.details = "Song Select (Play)"; break;
case GameState.SelectDrawings: activity.details = "Select Drawings"; break;
case GameState.Update: activity.details = "Update"; break;
case GameState.Busy: activity.details = "Busy"; break;
case GameState.Lobby: activity.details = "Lobby"; break;
case GameState.MatchSetup: activity.details = "Match Setup"; break;
case GameState.SelectMulti: activity.details = "Select Multi"; break;
case GameState.RankingVs: activity.details = "Ranking Vs"; break;
case GameState.OnlineSelection: activity.details = "Online Selection"; break;
case GameState.OptionsOffsetWizard: activity.details = "Options Offset Wizard"; break;
case GameState.RankingTagCoop: activity.details = "Ranking Tag Coop"; break;
case GameState.RankingTeam: activity.details = "Ranking Team"; break;
case GameState.BeatmapImport: activity.details = "Beatmap Import"; break;
case GameState.PackageUpdater: activity.details = "Package Updater"; break;
case GameState.Benchmark: activity.details = "Benchmark"; break;
case GameState.Tourney: activity.details = "Tourney"; break;
case GameState.Charts: activity.details = "Charts"; break;
}
switch (profile.banchoStatus.number) {
case BanchoStatusEnum.Idle: activity.state = "Idle"; break;
case BanchoStatusEnum.Afk: activity.state = "AFK"; break;
case BanchoStatusEnum.Playing: activity.state = "Playing"; break;
case BanchoStatusEnum.Editing: activity.state = "Editing"; break;
case BanchoStatusEnum.Modding: activity.state = "Modding"; break;
case BanchoStatusEnum.Multiplayer: activity.state = "Multiplayer"; break;
case BanchoStatusEnum.Watching: activity.state = "Watching"; break;
case BanchoStatusEnum.Testing: activity.state = "Testing"; break;
case BanchoStatusEnum.Submitting: activity.state = "Submitting"; break;
case BanchoStatusEnum.Paused: activity.state = "Paused"; break;
case BanchoStatusEnum.Lobby: activity.state = "Lobby"; break;
case BanchoStatusEnum.Multiplaying: activity.state = "Multiplaying"; break;
case BanchoStatusEnum.OsuDirect: activity.state = "osu!direct"; break;
}
break;
}
if (beatmap.set > 0) {
const mapBg = await getAsset(`https://assets.ppy.sh/beatmaps/${beatmap.set}/covers/list@2x.jpg`);
const res = await fetch(mapBg.replace(/^mp:/, "https://media.discordapp.net/"), { method: "HEAD" });
if (res.ok) activity.assets.large_image = mapBg;
}
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity, socketId: "tosu" });
}
function throttle<T extends Function>(func: T, limit: number, timedOutCallback?: () => void): T {
let inThrottle: boolean;
let callbackTimeout: NodeJS.Timeout;
return function (this: any, ...args: any[]) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
if (timedOutCallback) {
clearTimeout(callbackTimeout);
callbackTimeout = setTimeout(timedOutCallback, limit * 2);
}
}
} as any;
}
async function getAsset(key: string): Promise<string> {
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
return (await ApplicationAssetUtils.fetchAssetIds(OSU_APP_ID, [key]))[0];
}

View file

@ -0,0 +1,652 @@
/*
* 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<string>;
name: string;
application_id: string;
metadata?: {
button_urls?: Array<string>;
};
type: ActivityType;
url?: string;
flags: number;
}
export enum ActivityType {
PLAYING = 0,
STREAMING = 1,
LISTENING = 2,
WATCHING = 3,
COMPETING = 5
}
export enum BeatmapStatuses {
Unknown,
NotSubmitted = 1,
Pending = 2,
Ranked = 4,
Approved = 5,
Qualified = 6,
Loved = 7
}
export enum Modes {
Osu = 0,
Taiko = 1,
Fruits = 2,
Mania = 3
}
export enum BanchoStatusEnum {
Idle,
Afk,
Playing,
Editing,
Modding,
Multiplayer,
Watching,
Unknown,
Testing,
Submitting,
Paused,
Lobby,
Multiplaying,
OsuDirect
}
export enum UserLoginStatus {
Reconnecting = 0,
Guest = 256,
Recieving_data = 257,
Disconnected = 65537,
Connected = 65793
}
export enum ReleaseStream {
CuttingEdge,
Stable,
Beta,
Fallback
}
export enum ScoreMeterType {
None,
Colour,
Error
}
export enum LeaderboardType {
Local,
Global,
Selectedmods,
Friends,
Country
}
export enum GroupType {
None,
Artist,
BPM,
Creator,
Date,
Difficulty,
Length,
Rank,
MyMaps,
Search = 12,
Show_All = 12,
Title,
LastPlayed,
OnlineFavourites,
ManiaKeys,
Mode,
Collection,
RankedStatus
}
export enum SortType {
Artist,
BPM,
Creator,
Date,
Difficulty,
Length,
Rank,
Title
}
export enum ChatStatus {
Hidden,
Visible,
VisibleWithFriendsList
}
export enum ProgressBarType {
Off,
Pie,
TopRight,
BottomRight,
Bottom
}
export enum GameState {
Menu,
Edit,
Play,
Exit,
SelectEdit,
SelectPlay,
SelectDrawings,
ResultScreen,
Update,
Busy,
Unknown,
Lobby,
MatchSetup,
SelectMulti,
RankingVs,
OnlineSelection,
OptionsOffsetWizard,
RankingTagCoop,
RankingTeam,
BeatmapImport,
PackageUpdater,
Benchmark,
Tourney,
Charts
}
export type ApiAnswer = TosuApi | { error?: string; };
export type ApiAnswerPrecise = TosuPreciseAnswer | { error?: string; };
export interface TosuApi {
state: NumberName;
session: Session;
settings: Settings;
profile: Profile;
beatmap: Beatmap;
play: Play;
leaderboard: Leaderboard[];
performance: Performance;
resultsScreen: ResultsScreen;
folders: Folders;
files: Files;
directPath: DirectPath;
tourney: Tourney | undefined;
}
export interface BeatmapTime {
live: number;
firstObject: number;
lastObject: number;
mp3Length: number;
}
export interface Session {
playTime: number;
playCount: number;
}
export interface Settings {
interfaceVisible: boolean;
replayUIVisible: boolean;
chatVisibilityStatus: NumberName;
leaderboard: SettingsLeaderboard;
progressBar: NumberName;
bassDensity: number;
resolution: Resolution;
client: Client;
scoreMeter: ScoreMeter;
cursor: Cursor;
mouse: Mouse;
mania: Mania;
sort: NumberName;
group: NumberName;
skin: Skin;
mode: NumberName;
audio: Audio;
background: Background;
keybinds: Keybinds;
}
export interface Keybinds {
osu: KeybindsOsu;
fruits: KeybindsFruits;
taiko: KeybindsTaiko;
quickRetry: string;
}
export interface KeybindsOsu {
k1: string;
k2: string;
smokeKey: string;
}
export interface KeybindsFruits {
k1: string;
k2: string;
Dash: string;
}
export interface KeybindsTaiko {
innerLeft: string;
innerRight: string;
outerLeft: string;
outerRight: string;
}
export interface Volume {
master: number;
music: number;
effect: number;
}
export interface Audio {
ignoreBeatmapSounds: boolean;
useSkinSamples: boolean;
volume: Volume;
offset: Offset;
}
export interface Background {
storyboard: boolean;
video: boolean;
dim: number;
}
export interface Client {
updateAvailable: boolean;
branch: number;
version: string;
}
export interface Resolution {
fullscreen: boolean;
width: number;
height: number;
widthFullscreen: number;
heightFullscreen: number;
}
export interface Offset {
universal: number;
}
export interface Cursor {
useSkinCursor: boolean;
autoSize: boolean;
size: number;
}
export interface Mouse {
disableButtons: boolean;
disableWheel: boolean;
rawInput: boolean;
sensitivity: number;
}
export interface Mania {
speedBPMScale: boolean;
usePerBeatmapSpeedScale: boolean;
}
export interface Skin {
useDefaultSkinInEditor: boolean;
ignoreBeatmapSkins: boolean;
tintSliderBall: boolean;
useTaikoSkin: boolean;
name: string;
}
export interface SettingsLeaderboard {
visible: boolean;
type: NumberName;
}
export interface ScoreMeter {
type: NumberName;
size: number;
}
export interface Volume {
master: number;
music: number;
effect: number;
}
export interface NumberName {
number: number;
name: string;
}
export interface Profile {
userStatus: NumberName;
banchoStatus: NumberName;
id: number;
name: string;
mode: NumberName;
rankedScore: number;
level: number;
accuracy: number;
pp: number;
playCount: number;
globalRank: number;
countryCode: NumberName;
backgroundColour: string;
}
export interface Beatmap {
time: BeatmapTime;
status: NumberName;
checksum: string;
id: number;
set: number;
mode: NumberName;
artist: string;
artistUnicode: string;
title: string;
titleUnicode: string;
mapper: string;
version: string;
stats: Stats;
}
export interface Stats {
stars: Stars;
ar: Ar;
cs: Cs;
od: Od;
hp: Hp;
bpm: Bpm;
objects: Objects;
maxCombo: number;
}
export interface Stars {
live: number;
aim: number | undefined;
speed: number | undefined;
flashlight: number | undefined;
sliderFactor: number | undefined;
stamina: number | undefined;
rhythm: number | undefined;
color: number | undefined;
peak: number | undefined;
hitWindow: number | undefined;
total: number;
}
export interface Ar {
original: number;
converted: number;
}
export interface Cs {
original: number;
converted: number;
}
export interface Od {
original: number;
converted: number;
}
export interface Hp {
original: number;
converted: number;
}
export interface Bpm {
common: number;
min: number;
max: number;
}
export interface Objects {
circles: number;
sliders: number;
spinners: number;
holds: number;
total: number;
}
export interface Play {
playerName: string;
mode: NumberName;
score: number;
accuracy: number;
healthBar: HealthBar;
hits: Hits;
hitErrorArray: any[];
combo: Combo;
mods: NumberName;
rank: Rank;
pp: Pp;
unstableRate: number;
}
export interface HealthBar {
normal: number;
smooth: number;
}
export interface Hits {
"0": number;
"50": number;
"100": number;
"300": number;
geki: number;
katu: number;
sliderBreaks: number;
}
export interface Combo {
current: number;
max: number;
}
export interface Rank {
current: string;
maxThisPlay: string;
}
export interface Pp {
current: number;
fc: number;
maxAchievedThisPlay: number;
}
export interface Pp2 {
current: number;
fc: number;
}
export interface Leaderboard {
isFailed: boolean;
position: number;
team: number;
name: string;
score: number;
accuracy: number;
hits: Hits2;
combo: Combo2;
mods: NumberName;
rank: string;
}
export interface Hits2 {
"0": number;
"50": number;
"100": number;
"300": number;
geki: number;
katu: number;
}
export interface Combo2 {
current: number;
max: number;
}
export interface TosuPreciseAnswer {
currentTime: number;
keys: KeyOverlay;
hitErrors: number[];
tourney: PreciseTourney[];
}
export interface PreciseTourney {
ipcId: number;
keys: KeyOverlay;
hitErrors: number[];
}
interface KeyOverlay {
k1: KeyOverlayButton;
k2: KeyOverlayButton;
m1: KeyOverlayButton;
m2: KeyOverlayButton;
}
interface KeyOverlayButton {
isPressed: boolean;
count: number;
}
export interface Performance {
accuracy: Accuracy;
graph: Graph;
}
export interface Accuracy {
"95": number;
"96": number;
"97": number;
"98": number;
"99": number;
"100": number;
}
export interface Graph {
series: Series[];
xaxis: number[];
}
export interface Series {
name: string;
data: number[];
}
export interface ResultsScreen {
playerName: string;
mode: NumberName;
score: number;
accuracy: number;
name: string;
hits: Hits3;
mods: NumberName;
maxCombo: number;
rank: string;
pp: Pp2;
createdAt: string;
}
export interface Hits3 {
"0": number;
"50": number;
"100": number;
"300": number;
geki: number;
katu: number;
}
export interface Folders {
game: string;
skin: string;
songs: string;
beatmap: string;
}
export interface Files {
beatmap: string;
background: string;
audio: string;
}
export interface DirectPath {
beatmapFile: string;
beatmapBackground: string;
beatmapAudio: string;
beatmapFolder: string;
skinFolder: string;
}
export interface Tourney {
scoreVisible: boolean;
starsVisible: boolean;
ipcState: number;
bestOF: number;
team: {
left: string;
right: string;
};
points: {
left: number;
right: number;
};
totalScore: {
left: number;
right: number;
};
chat: TourneyChatMessages[];
clients: TourneyClients[];
}
export interface TourneyChatMessages {
team: string;
name: string;
message: string;
timestamp: string;
}
export interface TourneyClients {
ipcId: number;
team: "left" | "right";
user: {
id: number;
name: string;
country: string;
accuracy: number;
rankedScore: number;
playCount: number;
globalRank: number;
totalPP: number;
};
play: Play;
}

View file

@ -0,0 +1,77 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./style.css";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccessories";
import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { ChannelStore, Menu } from "@webpack/common";
import { settings } from "./settings";
import { Accessory, handleTranslate } from "./utils/accessory";
import { Icon } from "./utils/icon";
const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => {
if (!message.content) return;
const group = findGroupChildrenByChildId("copy-text", children);
if (!group) return;
group.splice(group.findIndex(c => c?.props?.id === "copy-text") + 1, 0, (
<Menu.MenuItem
id="ec-trans"
label="Translate"
icon={Icon}
action={() => handleTranslate(message)}
/>
));
};
export default definePlugin({
name: "Translate+",
description: "Vencord's translate plugin but with support for artistic languages!",
dependencies: ["MessageAccessoriesAPI"],
authors: [Devs.Ven, EquicordDevs.Prince527],
settings,
contextMenus: {
"message": messageCtxPatch
},
start() {
addMessageAccessory("ec-translation", props => <Accessory message={props.message} />);
addMessagePopoverButton("ec-translate", message => {
if (!message.content) return null;
return {
label: "Translate",
icon: Icon,
message: message,
channel: ChannelStore.getChannel(message.channel_id),
onClick: () => handleTranslate(message),
};
});
},
stop() {
removeMessagePopoverButton("ec-translate");
removeMessageAccessory("ec-translation");
}
});

View file

@ -0,0 +1,173 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
To generate:
- Visit https://translate.google.com/?sl=auto&tl=en&op=translate
- Open Language dropdown
- Open Devtools and use the element picker to pick the root of the language picker
- Right click on the element in devtools and click "Store as global variable"
copy(Object.fromEntries(
Array.from(
temp1.querySelectorAll("[data-language-code]"),
e => [e.dataset.languageCode, e.children[1].textContent]
).sort((a, b) => a[1] === "Detect language" ? -1 : b[1] === "Detect language" ? 1 : a[1].localeCompare(b[1]))
))
*/
export type languages = keyof typeof languages;
export const languages = {
"auto": "Detect language",
"af": "Afrikaans",
"sq": "Albanian",
"am": "Amharic",
"ar": "Arabic",
"hy": "Armenian",
"as": "Assamese",
"ay": "Aymara",
"az": "Azerbaijani",
"bm": "Bambara",
"eu": "Basque",
"be": "Belarusian",
"bn": "Bengali",
"bho": "Bhojpuri",
"bs": "Bosnian",
"bg": "Bulgarian",
"ca": "Catalan",
"ceb": "Cebuano",
"ny": "Chichewa",
"zh-CN": "Chinese (Simplified)",
"zh-TW": "Chinese (Traditional)",
"co": "Corsican",
"hr": "Croatian",
"cs": "Czech",
"da": "Danish",
"dv": "Dhivehi",
"doi": "Dogri",
"nl": "Dutch",
"en": "English",
"eo": "Esperanto",
"et": "Estonian",
"ee": "Ewe",
"tl": "Filipino",
"tp": "Toki Pona",
"sh": "Shavian",
"fi": "Finnish",
"fr": "French",
"fy": "Frisian",
"gl": "Galician",
"ka": "Georgian",
"de": "German",
"el": "Greek",
"gn": "Guarani",
"gu": "Gujarati",
"ht": "Haitian Creole",
"ha": "Hausa",
"haw": "Hawaiian",
"iw": "Hebrew",
"hi": "Hindi",
"hmn": "Hmong",
"hu": "Hungarian",
"is": "Icelandic",
"ig": "Igbo",
"ilo": "Ilocano",
"id": "Indonesian",
"ga": "Irish",
"it": "Italian",
"ja": "Japanese",
"jw": "Javanese",
"kn": "Kannada",
"kk": "Kazakh",
"km": "Khmer",
"rw": "Kinyarwanda",
"gom": "Konkani",
"ko": "Korean",
"kri": "Krio",
"ku": "Kurdish (Kurmanji)",
"ckb": "Kurdish (Sorani)",
"ky": "Kyrgyz",
"lo": "Lao",
"la": "Latin",
"lv": "Latvian",
"ln": "Lingala",
"lt": "Lithuanian",
"lg": "Luganda",
"lb": "Luxembourgish",
"mk": "Macedonian",
"mai": "Maithili",
"mg": "Malagasy",
"ms": "Malay",
"ml": "Malayalam",
"mt": "Maltese",
"mi": "Maori",
"mr": "Marathi",
"mni-Mtei": "Meiteilon (Manipuri)",
"lus": "Mizo",
"mn": "Mongolian",
"my": "Myanmar (Burmese)",
"ne": "Nepali",
"no": "Norwegian",
"or": "Odia (Oriya)",
"om": "Oromo",
"ps": "Pashto",
"fa": "Persian",
"pl": "Polish",
"pt": "Portuguese",
"pa": "Punjabi",
"qu": "Quechua",
"ro": "Romanian",
"ru": "Russian",
"sm": "Samoan",
"sa": "Sanskrit",
"gd": "Scots Gaelic",
"nso": "Sepedi",
"sr": "Serbian",
"st": "Sesotho",
"sn": "Shona",
"sd": "Sindhi",
"si": "Sinhala",
"sk": "Slovak",
"sl": "Slovenian",
"so": "Somali",
"es": "Spanish",
"su": "Sundanese",
"sw": "Swahili",
"sv": "Swedish",
"tg": "Tajik",
"ta": "Tamil",
"tt": "Tatar",
"te": "Telugu",
"th": "Thai",
"ti": "Tigrinya",
"ts": "Tsonga",
"tr": "Turkish",
"tk": "Turkmen",
"ak": "Twi",
"uk": "Ukrainian",
"ur": "Urdu",
"ug": "Uyghur",
"uz": "Uzbek",
"vi": "Vietnamese",
"cy": "Welsh",
"xh": "Xhosa",
"yi": "Yiddish",
"yo": "Yoruba",
"zu": "Zulu"
} as const;

View file

@ -0,0 +1,19 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
export const cl = classNameFactory("eq-trans-");
export interface Translation {
text: string;
src: string;
}
export type IconProps = {
width?: number;
height?: number;
};

View file

@ -0,0 +1,35 @@
/*
* 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 { OptionType } from "@utils/types";
export const settings = definePluginSettings({
target: {
type: OptionType.STRING,
description: "Target language",
default: "en",
restartNeeded: true
},
toki: {
type: OptionType.BOOLEAN,
description: "Enable Toki Pona",
default: true,
restartNeeded: true
},
sitelen: {
type: OptionType.BOOLEAN,
description: "Enable Sitelen Pona",
default: true,
restartNeeded: true
},
shavian: {
type: OptionType.BOOLEAN,
description: "Enable Shavian",
default: true,
restartNeeded: true
}
});

View file

@ -0,0 +1,20 @@
.eq-trans-accessory {
color: var(--text-muted);
margin-top: 0.5em;
font-style: italic;
font-weight: 400;
}
.eq-trans-accessory svg {
margin-right: 0.25em;
}
.eq-trans-dismiss {
all: unset;
cursor: pointer;
color: var(--text-link);
}
.eq-trans-dismiss:is(:hover, :focus) {
text-decoration: underline;
}

View file

@ -0,0 +1,42 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Parser, useEffect, useState } from "@webpack/common";
import { Message } from "discord-types/general";
import { languages } from "../misc/languages";
import { cl, Translation } from "../misc/types";
import { Icon } from "./icon";
import { translate } from "./translator";
const setters = new Map();
export function Accessory({ message }: { message: Message; }) {
const [translation, setTranslation] = useState<Translation | undefined>(undefined);
useEffect(() => {
if ((message as any).vencordEmbeddedBy) return;
setters.set(message.id, setTranslation);
return () => void setters.delete(message.id);
}, []);
if (!translation) return null;
return (
<span className={cl("accessory")}>
<Icon width={16} height={16} />
{Parser.parse(translation.text)}
{" "}
(translated from {languages[translation.src] ?? translation.src} - <button onClick={() => setTranslation(undefined)} className={cl("dismiss")}>Dismiss</button>)
</span>
);
}
export async function handleTranslate(message: Message) {
setters.get(message.id)!(await translate(message.content));
}

View file

@ -0,0 +1,17 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { cl, IconProps } from "../misc/types";
export function Icon({ width = 24, height = 24 }: IconProps) {
return (
<svg viewBox="0 96 960 960" height={width} width={height} className={cl("icon")}>
<path fill="currentColor" d="m475 976 181-480h82l186 480h-87l-41-126H604l-47 126h-82Zm151-196h142l-70-194h-2l-70 194Zm-466 76-55-55 204-204q-38-44-67.5-88.5T190 416h87q17 33 37.5 62.5T361 539q45-47 75-97.5T487 336H40v-80h280v-80h80v80h280v80H567q-22 69-58.5 135.5T419 598l98 99-30 81-127-122-200 200Z" />
<path fill="currentColor" d="m 830.17456,136.43701 c -11.54729,0 -20.84473,8.71252 -20.84473,19.53369 v 66.21826 h -66.21826 c -10.82122,0 -19.53369,9.29373 -19.53369,20.84107 0,11.54734 8.71247,20.84473 19.53369,20.84473 h 66.21826 v 66.21826 c 0,10.8212 9.29742,19.53369 20.84473,19.53369 11.54731,0 20.84106,-8.71249 20.84106,-19.53369 v -66.21826 h 66.21827 c 10.82124,0 19.53369,-9.29737 19.53369,-20.84473 0,-11.54736 -8.71245,-20.84107 -19.53369,-20.84107 H 851.01562 V 155.9707 c 0,-10.82117 -9.29377,-19.53369 -20.84106,-19.53369 z" />
<rect fill="currentColor" width="0.42110577" height="2.1055288" x="848.52814" y="112.42313" ry="0.2105529" />
</svg>
);
}

View file

@ -0,0 +1,135 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { settings } from "../settings";
function isTokiPona(text: string) {
const dictionary = /\b(?:leko|weka|pan|lete|linja|lipu|suli|nimi|akesi|misikeke|selo|ike|sijelo|sona|lili|pimeja|ante|jo|loje|telo|walo|kijetesantakalu|kasi|waso|wile|utala|lukin|sina|lape|ma|pilin|jasima|la|olin|pipi|meso|lawa|pi|pakala|oko|tan|ken|jaki|unpa|esun|seme|sitelen|len|kule|soko|open|ala|tenpo|lon|sinpin|pini|kokosila|mama|musi|monsi|mewika|taso|ona|mun|kiwen|tomo|mute|mi|nena|palisa|meli|laso|wawa|ale|kipisi|kulupu|ilo|lupa|nanpa|en|mu|jelo|kili|tonsi|moku|ni|kama|pu|poki|monsuta|sin|lasina|poka|soweli|sewi|elena|epiku|moli|pona|lanpan|alasa|anu|kute|uta|luka|suno|sama|awen|namako|suwi|noka|seli|mije|sike|jan|pali|tawa|inli|nasa|mani|wan|insa|nijon|nasin|kalama|ijo|toki|anpa|kala|kepeken|ko|kon|pana|tu|supa|kin|usawi|yupekosi)\b/gm;
return (text.match(dictionary) || []).length >= text.split(/\s+/).length * 0.5;
}
function isSitelen(text: string) {
const dictionary = /(?:󱤀|󱤁|󱤂|󱤃|󱤄|󱤅|󱤆|󱤇|󱤈|󱤉|󱤊|󱤋|󱤌|󱤍|󱤎|󱤏|󱤐|󱤑|󱤒|󱤓|󱤔|󱤕|󱤖|󱤗|󱤘|󱤙|󱤚|󱤛|󱤜|󱤝|󱤞|󱤟|󱤠|󱤡|󱤢|󱤣|󱤤|󱤥|󱤦|󱤧|󱤨|󱤩|󱤪|󱤫|󱤬|󱤭|󱤮|󱤯|󱤰|󱤱|󱤲|󱤳|󱤴|󱤵|󱤶|󱤷|󱤸|󱤹|󱤺|󱤻|󱤼|󱤽|󱤾|󱤿|󱥀|󱥁|󱥂|󱥃|󱥄|󱥅|󱥆|󱥇|󱥈|󱥉|󱥊|󱥋|󱥌|󱥍|󱥎|󱥏|󱥐|󱥑|󱥒|󱥓|󱥔|󱥕|󱥖|󱥗|󱥘|󱥙|󱥚|󱥛|󱥜|󱥝|󱥞|󱥟|󱥠|󱥡|󱥢|󱥣|󱥤|󱥥|󱥦|󱥧|󱥨|󱥩|󱥪|󱥫|󱥬|󱥭|󱥮|󱥯|󱥰|󱥱|󱥲|󱥳|󱥴|󱥵|󱥶|󱥷|󱦠|󱦡|󱦢|󱦣|󱥸|󱥹|󱥺|󱥻|󱥼|󱥽|󱥾|󱥿|󱦀|󱦁|󱦂|󱦃|󱦄|󱦅|󱦆|󱦇|󱦈|󱦐|󱦑|󱦒|󱦓|󱦔|󱦕|󱦖|󱦗|󱦘|󱦙|󱦚|󱦛|󱦜|󱦝)/gm;
return dictionary.test(text);
}
function isShavian(text: string) {
const shavianRegex = /[\u{10450}-\u{1047F}]+/u;
return shavianRegex.test(text);
}
async function translateShavian(message: string) {
const dictionary = await (await fetch("https://raw.githubusercontent.com/ForkPrince/TranslatePlus/322199d5fdb1a9506591c9f4a2826338b5d67e38/shavian.json")).json();
const punctuationMap = {
'"': "\"",
"«": "\"",
"»": "\"",
",": ",",
"!": "!",
"?": "?",
".": ".",
"(": "(",
")": ")",
"/": "/",
";": ";",
":": ":"
};
let translated = "";
const words = message.split(/\s+/);
for (let word of words) {
let punctuationBefore = "", punctuationAfter = "";
if (word[0] in punctuationMap) {
punctuationBefore = punctuationMap[word[0]];
word = word.slice(1);
}
if (word[word.length - 1] in punctuationMap) {
punctuationAfter = punctuationMap[word[word.length - 1]];
word = word.slice(0, -1);
}
translated += punctuationBefore;
if (word in dictionary) translated += dictionary[word];
else translated += word;
translated += punctuationAfter + " ";
}
return translated.trim();
}
async function translateSitelen(message: string) {
message = Array.from(message).join(" ");
const dictionary = await (await fetch("https://raw.githubusercontent.com/ForkPrince/TranslatePlus/5ca152b134ea11433971f21b2ef8d556d4306717/sitelen-pona.json")).json();
const sorted = Object.keys(dictionary).sort((a, b) => b.length - a.length);
const pattern = new RegExp(`(${sorted.join("|")})`, "g");
const translate = message.replace(pattern, match => dictionary[match]);
return translate;
}
async function google(target: string, text: string) {
const translate = await (await fetch(`https://translate.googleapis.com/translate_a/single?${new URLSearchParams({ client: "gtx", sl: "auto", tl: target, dt: "t", dj: "1", source: "input", q: text })}`)).json();
return {
src: translate.src,
text: translate.sentences.map(s => s.trans).filter(Boolean).join("")
};
}
export async function translate(text: string): Promise<any> {
const { target, toki, sitelen, shavian } = settings.store;
const output = { src: "", text: "" };
if ((isTokiPona(text) || isSitelen(text)) && (toki || sitelen)) {
if (isSitelen(text) && sitelen) text = await translateSitelen(text);
console.log(text);
const translate = await (await fetch("https://aiapi.serversmp.xyz/toki", {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({
text: text,
src: "tl",
target: "en"
})
})).json();
console.log(translate);
output.src = "tp";
output.text = target === "en" ? translate.translation[0] : (await google(target, translate.translation[0])).text;
} else if (isShavian(text) && shavian) {
const translate = await translateShavian(text);
output.src = "sh";
output.text = target === "en" ? translate : (await google(target, translate)).text;
} else {
const translate = await google(target, text);
output.src = translate.src;
output.text = translate.text;
}
return output;
}

View file

@ -18,53 +18,7 @@ const MediaScriptsAndCssSrc = [...MediaAndCssSrc, "script-src", "worker-src"];
// script and just adding to it. But generally, you should just edit this file instead
export const CspPolicies: PolicyMap = {
"*.github.io": MediaAndCssSrc, // github pages, used by most themes
"raw.githubusercontent.com": MediaAndCssSrc, // github raw, used by some themes
"*.gitlab.io": MediaAndCssSrc, // gitlab pages, used by some themes
"gitlab.com": MediaAndCssSrc, // gitlab raw, used by some themes
"*.codeberg.page": MediaAndCssSrc, // codeberg pages, used by some themes
"codeberg.org": MediaAndCssSrc, // codeberg raw, used by some themes
"*.githack.com": MediaAndCssSrc, // githack (namely raw.githack.com), used by some themes
"jsdelivr.net": MediaAndCssSrc, // jsdeliver, used by very few themes
"fonts.googleapis.com": CssSrc, // google fonts, used by many themes
"i.imgur.com": MediaSrc, // imgur, used by some themes
"i.ibb.co": MediaSrc, // imgbb, used by some themes
"cdn.discordapp.com": MediaAndCssSrc, // Discord CDN, used by Vencord and some themes to load media
"media.discordapp.net": MediaSrc, // Discord media CDN, possible alternative to Discord CDN
// CDNs used for some things by Vencord.
// FIXME: we really should not be using CDNs anymore
"cdnjs.cloudflare.com": MediaScriptsAndCssSrc,
"cdn.jsdelivr.net": MediaScriptsAndCssSrc,
// Function Specific
"api.github.com": ConnectSrc, // used for updating Vencord itself
"ws.audioscrobbler.com": ConnectSrc, // last.fm API
"translate.googleapis.com": ConnectSrc, // Google Translate API
"*.vencord.dev": MediaSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev)
"manti.vendicated.dev": MediaSrc, // ReviewDB API
"decor.fieryflames.dev": ConnectSrc, // Decor API
"ugc.decor.fieryflames.dev": MediaSrc, // Decor CDN
"sponsor.ajay.app": ConnectSrc, // Dearrow API
"dearrow-thumb.ajay.app": MediaSrc, // Dearrow Thumbnail CDN
"usrbg.is-hardly.online": MediaSrc, // USRBG API
"icons.duckduckgo.com": MediaSrc, // DuckDuckGo Favicon API (Reverse Image Search)
// Equicord
"cdn.nest.rip": MediaSrc, // Nest CDN
"equicord.org": MediaSrc, // Equicord CDN
"*.equicord.org": MediaSrc, // Equicord CDN
"discord-themes.com": MediaAndCssSrc, // Discord Themes CDN
"fonts.google.com": ConnectSrc,
"lrclib.net": ConnectSrc, // Lrclib API
"spotify-lyrics-api-pi.vercel.app": ConnectSrc, // Spotify Lyrics API
"stats.fm": MediaSrc, // Stats.fm API
"discord.com": MediaScriptsAndCssSrc, // Discord
"*.discord.com": MediaScriptsAndCssSrc // Discord
"*": MediaScriptsAndCssSrc
};
const findHeader = (headers: PolicyMap, headerName: Lowercase<string>) => {