Add: snowfall (#474)
* [Feat] Add Plugin: Snowfall * Add a plugin called Snowfall, based on the BetterDiscord plugin by the same name. Lets snow fall from the top of your discord client. * [QOL] snowfall: Credit BetterDiscord Author * Here Comes Santa Claus * [Fix] snowfall: Fix Several Bugs * Switch from HTML entity on the content option to the Unicode character. * Fixed type switching bug that causes memory leaks. * Fixed image loading bug. * Fixed content array handing for image sources. * [Feat/QOL] snowfall: Configurability Overhaul * Fixed switching between types. * Created settings option for user to select type. * Created description to warn of possible performance issues. * Created settings options for users to set speed, amount, and size. * Created custom listener to check for settings changes. * Added hardcoded SVGs for Snowflakes when using the image option (including a custom-made discord one as an easter-egg). * Fix condition for deviation check in random function --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
parent
871de99ad7
commit
f3ac9ff64a
3 changed files with 496 additions and 0 deletions
491
src/equicordplugins/snowfall/index.tsx
Normal file
491
src/equicordplugins/snowfall/index.tsx
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Heading } from "@components/Heading";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { createRoot, React } from "@webpack/common";
|
||||
import type { Root } from "react-dom/client";
|
||||
|
||||
const SnowfallCSS = `
|
||||
#snowfield {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
z-index: 100000;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
.snowflake {
|
||||
position: absolute;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.snowflake-solid {
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
}
|
||||
.snowflake-image {
|
||||
filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.5));
|
||||
}
|
||||
`;
|
||||
|
||||
// SVG snowflake images as data URIs
|
||||
const SNOWFLAKE_SVGS = [
|
||||
// 6-pointed snowflake
|
||||
"data:image/svg+xml," + encodeURIComponent(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<path fill="white" d="M50,10 L55,45 L50,50 L45,45 Z M50,90 L55,55 L50,50 L45,55 Z M10,50 L45,55 L50,50 L45,45 Z M90,50 L55,55 L50,50 L55,45 Z M25,25 L45,45 L50,40 L40,30 Z M75,75 L55,55 L50,60 L60,70 Z M75,25 L55,45 L60,50 L70,40 Z M25,75 L45,55 L40,50 L30,60 Z"/>
|
||||
<circle cx="50" cy="50" r="8" fill="white"/>
|
||||
</svg>
|
||||
`),
|
||||
// 8-pointed star snowflake
|
||||
"data:image/svg+xml," + encodeURIComponent(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<g fill="white">
|
||||
<rect x="47" y="5" width="6" height="90" rx="2"/>
|
||||
<rect x="5" y="47" width="90" height="6" rx="2"/>
|
||||
<rect x="47" y="5" width="6" height="90" rx="2" transform="rotate(45 50 50)"/>
|
||||
<rect x="47" y="5" width="6" height="90" rx="2" transform="rotate(-45 50 50)"/>
|
||||
<circle cx="50" cy="50" r="10" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
`),
|
||||
// Discord snowflake
|
||||
"data:image/svg+xml," + encodeURIComponent(`
|
||||
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<symbol id="discord" viewBox="0 0 24 24">
|
||||
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/>
|
||||
</symbol>
|
||||
</defs>
|
||||
<!-- snowflake arms -->
|
||||
<line x1="100" y1="100" x2="150" y2="100" stroke="#FFF" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="100" y1="100" x2="125" y2="143.3" stroke="#FFF" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="100" y1="100" x2="75" y2="143.3" stroke="#FFF" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="100" y1="100" x2="50" y2="100" stroke="#FFF" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="100" y1="100" x2="75" y2="56.7" stroke="#FFF" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="100" y1="100" x2="125" y2="56.7" stroke="#FFF" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<!-- circle mask -->
|
||||
<mask id="logoMask">
|
||||
<rect width="200" height="200" fill="white"/>
|
||||
<circle cx="150" cy="100" r="18" fill="black"/> <!-- right -->
|
||||
<circle cx="125" cy="143.3" r="18" fill="black"/> <!-- top-right -->
|
||||
<circle cx="75" cy="143.3" r="18" fill="black"/> <!-- top-left -->
|
||||
<circle cx="50" cy="100" r="18" fill="black"/> <!-- left -->
|
||||
<circle cx="75" cy="56.7" r="18" fill="black"/> <!-- bottom-left -->
|
||||
<circle cx="125" cy="56.7" r="18" fill="black"/> <!-- bottom-right -->
|
||||
</mask>
|
||||
<!-- circle behind all elements -->
|
||||
<circle cx="100" cy="100" r="50" stroke="#FFF" stroke-width="1.5" fill="none" stroke-linecap="round" mask="url(#logoMask)" />
|
||||
<use href="#discord" fill="#FFF" x="135" y="85" width="30" height="30" transform="rotate(90, 150, 100)"/>
|
||||
<use href="#discord" fill="#FFF" x="110" y="128.3" width="30" height="30" transform="rotate(150, 125, 143.3)"/>
|
||||
<use href="#discord" fill="#FFF" x="60" y="128.3" width="30" height="30" transform="rotate(210, 75, 143.3)"/>
|
||||
<use href="#discord" fill="#FFF" x="35" y="85" width="30" height="30" transform="rotate(270, 50, 100)"/>
|
||||
<use href="#discord" fill="#FFF" x="60" y="41.7" width="30" height="30" transform="rotate(330, 75, 56.7)"/>
|
||||
<use href="#discord" fill="#FFF" x="110" y="41.7" width="30" height="30" transform="rotate(30, 125, 56.7)"/>
|
||||
</svg>
|
||||
`),
|
||||
];
|
||||
|
||||
const settings = definePluginSettings({
|
||||
typeOfSnow: {
|
||||
description: "Change the type of snow displayed (Affects performance).",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ label: "Solid (Highest Performance)", value: "solid" },
|
||||
{ label: "Text (Medium Performance)", value: "text", default: true },
|
||||
{ label: "Image (Lowest Performance)", value: "image" }
|
||||
],
|
||||
},
|
||||
maxSize: {
|
||||
description: "Maximum snowflake size",
|
||||
type: OptionType.SLIDER,
|
||||
default: 30,
|
||||
markers: [10, 20, 30, 40, 50]
|
||||
},
|
||||
speed: {
|
||||
description: "Snowfall speed (higher = faster fall)",
|
||||
type: OptionType.SLIDER,
|
||||
default: 50,
|
||||
markers: [50, 100, 200, 300, 400, 500]
|
||||
},
|
||||
flakesPerSecond: {
|
||||
description: "Snowflakes per second (higher = denser snowfall)",
|
||||
type: OptionType.SLIDER,
|
||||
default: 5,
|
||||
markers: [1, 5, 10, 20, 40, 60],
|
||||
min: 1,
|
||||
max: 60,
|
||||
}
|
||||
});
|
||||
|
||||
class CopleSnow {
|
||||
private static winWidth = window.innerWidth;
|
||||
private static winHeight = window.innerHeight;
|
||||
|
||||
static readonly defaultOptions = {
|
||||
minSize: 10,
|
||||
maxSize: 30,
|
||||
type: "text" as "text" | "solid" | "image",
|
||||
content: "❄" as string | string[],
|
||||
fadeOut: true,
|
||||
autoplay: true,
|
||||
interval: 200
|
||||
};
|
||||
|
||||
private static cssPrefix(propertyName: string): string | null {
|
||||
const capitalize = propertyName.charAt(0).toUpperCase() + propertyName.slice(1);
|
||||
const tempDiv = document.createElement("div");
|
||||
const { style } = tempDiv;
|
||||
const prefixes = ["Webkit", "Moz", "ms", "O"];
|
||||
|
||||
if (propertyName in style) return propertyName;
|
||||
for (const prefix of prefixes) {
|
||||
const name = prefix + capitalize;
|
||||
if (name in style) return name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static readonly cssPrefixedNames = {
|
||||
transform: this.cssPrefix("transform"),
|
||||
transition: this.cssPrefix("transition")
|
||||
};
|
||||
|
||||
private static readonly transitionEndEvent =
|
||||
{ WebkitTransition: "webkitTransitionEnd", OTransition: "oTransitionEnd", Moztransition: "transitionend", transition: "transitionend" }[
|
||||
this.cssPrefixedNames.transition ?? "transition"
|
||||
] ?? "transitionend";
|
||||
|
||||
private static random(min: number, max: number, deviation?: number): number {
|
||||
if (!deviation) {
|
||||
deviation *= max;
|
||||
max += deviation;
|
||||
min = max - deviation;
|
||||
} else {
|
||||
min = min || 0;
|
||||
}
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
}
|
||||
|
||||
private static setStyle(element: HTMLElement, rules: Record<string, string | number>) {
|
||||
for (const [name, value] of Object.entries(rules)) {
|
||||
const cssName = CopleSnow.cssPrefixedNames[name as keyof typeof CopleSnow.cssPrefixedNames] || name;
|
||||
(element.style as any)[cssName] = value;
|
||||
}
|
||||
}
|
||||
|
||||
private options = { ...CopleSnow.defaultOptions };
|
||||
private queue: HTMLElement[] = [];
|
||||
private $snowfield: HTMLDivElement;
|
||||
private timer: number | null = null;
|
||||
public playing = false;
|
||||
|
||||
constructor(newOptions: Partial<typeof CopleSnow.defaultOptions> = {}) {
|
||||
Object.assign(this.options, newOptions);
|
||||
|
||||
this.$snowfield = document.createElement("div");
|
||||
this.$snowfield.id = "snowfield";
|
||||
document.body.appendChild(this.$snowfield);
|
||||
|
||||
const updateSize = () => {
|
||||
CopleSnow.winHeight = window.innerHeight;
|
||||
CopleSnow.winWidth = window.innerWidth;
|
||||
};
|
||||
window.addEventListener("resize", updateSize);
|
||||
(this as any)._resizeHandler = updateSize;
|
||||
|
||||
this.$snowfield.addEventListener(CopleSnow.transitionEndEvent, e => {
|
||||
const snowflake = e.target as HTMLElement;
|
||||
if (snowflake.classList.contains("snowflake")) {
|
||||
this.$snowfield.removeChild(snowflake);
|
||||
this.queue.push(snowflake);
|
||||
}
|
||||
});
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.hidden) this.stop();
|
||||
else this.play();
|
||||
};
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
(this as any)._visibilityHandler = handleVisibilityChange;
|
||||
|
||||
if (this.options.autoplay) this.play();
|
||||
}
|
||||
|
||||
private createSnowflake(): HTMLElement {
|
||||
const { type, content } = this.options as { type: "text" | "image" | "solid"; content: string | string[]; };
|
||||
const cntLength = Array.isArray(content) ? content.length : 1;
|
||||
|
||||
let snowflake: HTMLElement;
|
||||
|
||||
if (type === "image") {
|
||||
snowflake = document.createElement("img");
|
||||
const src = typeof content === "string"
|
||||
? content
|
||||
: content[Math.floor(Math.random() * cntLength)];
|
||||
(snowflake as HTMLImageElement).src = src;
|
||||
snowflake.setAttribute("draggable", "false");
|
||||
} else {
|
||||
snowflake = document.createElement("div");
|
||||
if (type === "text") {
|
||||
const textContent = typeof content === "string"
|
||||
? content
|
||||
: content[Math.floor(Math.random() * cntLength)];
|
||||
snowflake.textContent = textContent;
|
||||
}
|
||||
// if type is solid we don't need to set content
|
||||
}
|
||||
|
||||
snowflake.className = `snowflake snowflake-${type}`;
|
||||
(snowflake as any).dataset.type = type;
|
||||
|
||||
return snowflake;
|
||||
}
|
||||
|
||||
private animateSnowflake() {
|
||||
const { winWidth, winHeight } = CopleSnow;
|
||||
const size = CopleSnow.random(this.options.minSize, this.options.maxSize);
|
||||
const top = -2 * size;
|
||||
const left = CopleSnow.random(0, winWidth - size);
|
||||
const opacity = CopleSnow.random(5, 10) / 10;
|
||||
const angle = CopleSnow.random(0, winHeight * 0.8, 1);
|
||||
const translateX = CopleSnow.random(-100, 100);
|
||||
const translateY = winHeight + size * 2;
|
||||
const baseSpeed = settings.store.speed;
|
||||
const duration = (winHeight * 10) / (baseSpeed / 50);
|
||||
let snowflake: HTMLElement;
|
||||
if (this.queue.length > 0) {
|
||||
const reused = this.queue.shift()!;
|
||||
if ((reused.dataset.type as string) !== this.options.type) {
|
||||
snowflake = this.createSnowflake();
|
||||
} else {
|
||||
snowflake = reused;
|
||||
if (this.options.type === "text") {
|
||||
const { content } = this.options;
|
||||
if (Array.isArray(content)) {
|
||||
snowflake.textContent = content[Math.floor(Math.random() * content.length)];
|
||||
}
|
||||
} else if (this.options.type === "image") {
|
||||
const { content } = this.options;
|
||||
if (Array.isArray(content)) {
|
||||
(snowflake as HTMLImageElement).src = content[Math.floor(Math.random() * content.length)];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
snowflake = this.createSnowflake();
|
||||
}
|
||||
|
||||
const styleRules: Record<string, string | number> = {
|
||||
top: `${top}px`,
|
||||
left: `${left}px`,
|
||||
opacity: opacity,
|
||||
transform: "none",
|
||||
transition: `${duration}ms linear`
|
||||
};
|
||||
|
||||
switch (this.options.type) {
|
||||
case "solid":
|
||||
styleRules.width = styleRules.height = `${size}px`;
|
||||
break;
|
||||
case "text":
|
||||
styleRules.fontSize = `${size}px`;
|
||||
break;
|
||||
case "image":
|
||||
styleRules.width = `${size}px`;
|
||||
styleRules.height = "auto";
|
||||
break;
|
||||
}
|
||||
|
||||
CopleSnow.setStyle(snowflake, styleRules);
|
||||
this.$snowfield.appendChild(snowflake);
|
||||
|
||||
setTimeout(() => {
|
||||
CopleSnow.setStyle(snowflake, {
|
||||
transform: `translate(${translateX}px, ${translateY}px) rotate(${angle}deg)`,
|
||||
opacity: this.options.fadeOut ? 0 : opacity
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
play() {
|
||||
if (this.playing) return;
|
||||
this.timer = window.setInterval(() => this.animateSnowflake(), this.options.interval);
|
||||
this.playing = true;
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (!this.playing) return;
|
||||
if (this.timer !== null) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
this.playing = false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.stop();
|
||||
if (this.$snowfield.parentNode) {
|
||||
this.$snowfield.remove();
|
||||
}
|
||||
window.removeEventListener("resize", (this as any)._resizeHandler);
|
||||
document.removeEventListener("visibilitychange", (this as any)._visibilityHandler);
|
||||
}
|
||||
|
||||
updateOptions(newOptions: Partial<typeof CopleSnow.defaultOptions>) {
|
||||
Object.assign(this.options, newOptions);
|
||||
}
|
||||
}
|
||||
|
||||
const SnowfallManager: React.FC = () => {
|
||||
const snowRef = React.useRef<CopleSnow | null>(null);
|
||||
const styleRef = React.useRef<HTMLStyleElement | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Inject CSS
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.id = "snowfall-styles";
|
||||
styleEl.textContent = SnowfallCSS;
|
||||
document.head.appendChild(styleEl);
|
||||
styleRef.current = styleEl;
|
||||
|
||||
// Get initial snow type from settings
|
||||
const snowType = settings.store.typeOfSnow as "text" | "solid" | "image";
|
||||
const snowOptions: Partial<typeof CopleSnow.defaultOptions> = {
|
||||
autoplay: false,
|
||||
type: snowType,
|
||||
maxSize: settings.store.maxSize,
|
||||
interval: 1000 / settings.store.flakesPerSecond
|
||||
};
|
||||
|
||||
snowOptions.interval = settings.store.speed;
|
||||
|
||||
// Set content based on type
|
||||
if (snowType === "image") {
|
||||
snowOptions.content = SNOWFLAKE_SVGS;
|
||||
}
|
||||
|
||||
// Create snow instance with settings
|
||||
const snow = new CopleSnow(snowOptions);
|
||||
snowRef.current = snow;
|
||||
|
||||
const blurHandler = () => snow.stop();
|
||||
const focusHandler = () => {
|
||||
if (document.hasFocus()) snow.play();
|
||||
};
|
||||
|
||||
window.addEventListener("blur", blurHandler);
|
||||
window.addEventListener("focus", focusHandler);
|
||||
|
||||
if (document.hasFocus()) {
|
||||
snow.play();
|
||||
}
|
||||
|
||||
// Custom listener for settings changes (we don't have a built-in one)
|
||||
let lastSettings = {
|
||||
type: settings.store.typeOfSnow as "text" | "solid" | "image",
|
||||
maxSize: settings.store.maxSize,
|
||||
speed: settings.store.speed,
|
||||
flakesPerSecond: settings.store.flakesPerSecond
|
||||
};
|
||||
|
||||
const settingsInterval = setInterval(() => {
|
||||
const newSettings = {
|
||||
type: settings.store.typeOfSnow as "text" | "solid" | "image",
|
||||
maxSize: settings.store.maxSize,
|
||||
speed: settings.store.speed,
|
||||
flakesPerSecond: settings.store.flakesPerSecond
|
||||
};
|
||||
|
||||
if (Object.keys(newSettings).some(k => newSettings[k as keyof typeof newSettings] !== lastSettings[k as keyof typeof lastSettings])) {
|
||||
lastSettings = newSettings;
|
||||
|
||||
const updateOptions: Partial<typeof CopleSnow.defaultOptions> = {
|
||||
type: newSettings.type,
|
||||
maxSize: newSettings.maxSize,
|
||||
interval: 1000 / newSettings.flakesPerSecond
|
||||
};
|
||||
|
||||
if (newSettings.type === "image") updateOptions.content = SNOWFLAKE_SVGS;
|
||||
else if (newSettings.type === "text") updateOptions.content = "❄";
|
||||
|
||||
snow.updateOptions(updateOptions);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
snow.destroy();
|
||||
snowRef.current = null;
|
||||
|
||||
window.removeEventListener("blur", blurHandler);
|
||||
window.removeEventListener("focus", focusHandler);
|
||||
|
||||
clearInterval(settingsInterval);
|
||||
|
||||
if (styleEl.parentNode) {
|
||||
styleEl.remove();
|
||||
}
|
||||
styleRef.current = null;
|
||||
|
||||
const snowfield = document.getElementById("snowfield");
|
||||
if (snowfield) snowfield.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
let snowRoot: Root | null = null;
|
||||
let container: HTMLDivElement | null = null;
|
||||
|
||||
export default definePlugin({
|
||||
name: "Snowfall",
|
||||
description: "Let it snow on Discord! Ported from the BetterDiscord plugin by square.",
|
||||
authors: [EquicordDevs.ZcraftElite, EquicordDevs.square],
|
||||
|
||||
settingsAboutComponent: () => (
|
||||
<>
|
||||
<Heading>Information</Heading>
|
||||
<Paragraph>
|
||||
This plugin adds a christmas-y snowfall effect on top of Discord's interface.
|
||||
You can change the type of snow in the settings below.
|
||||
<br /><br />
|
||||
NOTE: While on most computers this plugin will not impact performance any more than your average Equicord extension,
|
||||
it may cause some lag on lower end systems.
|
||||
</Paragraph>
|
||||
</>
|
||||
),
|
||||
|
||||
|
||||
settings,
|
||||
|
||||
start() {
|
||||
container = document.createElement("div");
|
||||
container.id = "snowfall-plugin-container";
|
||||
document.body.appendChild(container);
|
||||
|
||||
snowRoot = createRoot(container);
|
||||
snowRoot.render(React.createElement(SnowfallManager));
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (snowRoot) {
|
||||
snowRoot.unmount();
|
||||
snowRoot = null;
|
||||
}
|
||||
if (container) {
|
||||
if (container.parentNode) container.remove();
|
||||
container = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -143,6 +143,7 @@ export function initWs(isManual = false) {
|
|||
case "disable": {
|
||||
const m = d.data;
|
||||
const settings = Settings.plugins[m.pluginName];
|
||||
if (!settings) throw new Error("Plugin not found: " + m.pluginName);
|
||||
if (m.enabled !== settings.enabled)
|
||||
toggleEnabled(m.pluginName, reply);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1186,6 +1186,10 @@ export const EquicordDevs = Object.freeze({
|
|||
name: "Prism",
|
||||
id: 390884143749136386n
|
||||
},
|
||||
square: {
|
||||
name: "square",
|
||||
id: 219363409097916416n
|
||||
}
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue