feat: Lazy-load chroma/fuzzysort and add memory backend

Remove Shiggyenhancements plugin

Replace eager chroma and fuzzysort imports with lazy require/cached
loads to reduce startup bundle and runtime cost. Add createMemoryBackend
(an in-memory storage with debounced persistence) and use it for fonts
to accelerate startup. Add per-store write debouncers and change lodash
import to a type-only import
This commit is contained in:
kmmiio99o 2026-02-23 17:28:55 +01:00
parent 8857eaf144
commit 98e65fcefe
No known key found for this signature in database
GPG key ID: 163FE39F7772D2EC
11 changed files with 116 additions and 322 deletions

View file

@ -36,7 +36,6 @@ export const getCorePlugins = (): CorePluginRecord => ({
"bunny.notrack": makeLazyPlugin(() => require("./notrack")),
"bunny.messagefix": makeLazyPlugin(() => require("./messagefix")),
"bunny.fixembed": makeLazyPlugin(() => require("./fixembed")),
"bunny.enhancements": makeLazyPlugin(() => require("./shiggyenhancements")),
});
/**

View file

@ -1,301 +0,0 @@
import { defineCorePlugin } from "..";
import { findByProps } from "@metro";
import { logger } from "@lib/utils/logger";
import { settings } from "@lib/api/settings";
import { React } from "@metro/common";
const { TableRowGroup, TableRow, TableSwitchRow, Stack } = findByProps(
"TableRowGroup",
"TableRow",
"TableSwitchRow",
"Stack",
);
const { ScrollView } = require("react-native");
// Get ReactNative for NativeModules
const { ReactNative } = window as any;
const { DCDSoundManager } = ReactNative?.NativeModules || {};
// Sound settings type
type SoundSettings = {
enabled: boolean;
probability: number;
};
declare module "@lib/api/settings" {
interface Settings {
startupSound?: SoundSettings;
}
}
const soundUrl =
"https://github.com/kmmiio99o/kmmiio99o.github.io/raw/refs/heads/main/music/shiggycord.mp3";
const soundId = 6971;
let soundDuration = -1;
let timeoutId: NodeJS.Timeout | null = null;
let isPlaying = false;
let isPrepared = false;
// Function to prepare the sound
const prepareSound = function (): Promise<any> {
return new Promise(function (resolve) {
if (!DCDSoundManager) {
logger.error("DCDSoundManager not available");
resolve(null);
return;
}
DCDSoundManager.prepare(
soundUrl,
"music",
soundId,
function (error: any, sound: any) {
if (error) {
logger.error("Failed to prepare sound:", error);
resolve(null);
} else {
resolve(sound);
}
},
);
});
};
// Function to play the sound
async function playSound() {
if (!DCDSoundManager) {
logger.error("DCDSoundManager not available for playback");
return;
}
if (isPlaying) {
if (timeoutId != null) clearTimeout(timeoutId);
DCDSoundManager.stop(soundId);
isPlaying = false;
}
isPlaying = true;
try {
await DCDSoundManager.play(soundId);
if (soundDuration > 0) {
timeoutId = setTimeout(function () {
isPlaying = false;
DCDSoundManager.stop(soundId);
timeoutId = null;
}, soundDuration);
}
} catch (error) {
logger.error("Error playing sound:", error);
isPlaying = false;
}
}
// Check if we should play based on probability
function shouldPlaySound(): boolean {
const probability = settings.startupSound?.probability ?? 2;
const random = Math.random() * 100;
return random <= probability;
}
export default defineCorePlugin({
manifest: {
id: "bunny.enhancements",
version: "0.1.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "Shiggy Enhancements",
description:
"Fixes common discord bugs because discord wont, and adds some features. (Originally Kettu Enhancements)",
authors: [{ name: "cocobo1" }, { name: "Shiggy Team" }],
},
},
SettingsComponent() {
const { useState, useEffect } = React;
const [config, setConfig] = useState<SoundSettings>({
enabled: settings.startupSound?.enabled ?? true,
probability: settings.startupSound?.probability ?? 2,
});
useEffect(() => {
settings.startupSound = config;
}, [config]);
const updateConfig = (
key: keyof SoundSettings,
value: boolean | number,
) => {
setConfig((prev) => ({ ...prev, [key]: value }));
};
// Get UI components including TextInput
const {
TableRowGroup: _TRG,
TableRow: _TR,
TableSwitchRow: _TSR,
Stack: _S,
TextInput: _TextInput,
} = findByProps(
"TableRowGroup",
"TableRow",
"TableSwitchRow",
"Stack",
"TextInput",
);
// Fallback if table components aren't available
if (!_TRG || !_TSR || !_TR || !_S) {
const FallbackText =
"Startup Sound UI unavailable (missing TableRow components).";
return React.createElement(
ScrollView,
{ style: { flex: 1, padding: 12 } },
FallbackText,
);
}
const TableRowGroup = _TRG;
const TableRow = _TR;
const TableSwitchRow = _TSR;
const Stack = _S;
const TextInput = _TextInput;
// Handle probability input change
const handleProbabilityChange = (v: string) => {
const num = parseInt(v);
if (!isNaN(num) && num >= 0 && num <= 100) {
updateConfig("probability", num);
} else if (v === "") {
updateConfig("probability", 0);
}
};
// Create settings elements
const settingsElements = [];
// Startup Sound toggle with probability display
settingsElements.push(
React.createElement(
TableRowGroup,
{ title: "Startup Sound", key: "startup-sound" },
React.createElement(TableSwitchRow, {
label: "Enable Startup Sound",
value: config.enabled,
onValueChange: (v: boolean) => updateConfig("enabled", v),
}),
React.createElement(TableRow, {
label: "Play Chance",
subLabel: `Current: ${config.probability}% chance to play on startup`,
}),
),
);
// Probability input in its own invisible group
if (TextInput) {
settingsElements.push(
React.createElement(
TableRowGroup,
{ title: "", key: "probability-input-group" },
React.createElement(TextInput, {
placeholder: "Enter probability (0-100)",
value: config.probability.toString(),
onChange: handleProbabilityChange,
key: "probability-input",
}),
),
);
}
// About section
settingsElements.push(
React.createElement(
TableRowGroup,
{ title: "About", key: "about" },
React.createElement(TableRow, {
label: "Description",
subLabel:
"Shiggy Enhancements, more options, more features. Originally Kettu Enhancements with additinal features.",
disabled: true,
}),
),
);
return React.createElement(
ScrollView,
{ style: { flex: 1 } },
React.createElement(
Stack,
{ spacing: 8, style: { padding: 10 } },
...settingsElements,
),
);
},
start() {
logger.log("Shiggy Enhancements: Starting plugin");
// Initialize sound settings
settings.startupSound = settings.startupSound || {
enabled: false,
probability: 2,
};
// Check if DCDSoundManager is available
if (!DCDSoundManager) {
logger.error("DCDSoundManager not found - startup sound disabled");
return;
}
// Play startup sound if enabled and probability check passes
if (settings.startupSound.enabled && shouldPlaySound()) {
logger.log(
`Attempting to play startup sound (${settings.startupSound.probability}% chance)`,
);
// Always prepare the sound fresh each time
prepareSound()
.then(function (sound) {
if (sound) {
isPrepared = true;
soundDuration = sound.duration || 5000;
logger.log(`Sound prepared, duration: ${soundDuration}ms`);
playSound();
logger.log(
`Startup sound played (${settings.startupSound?.probability}% chance)`,
);
} else {
logger.error("Failed to prepare sound");
}
})
.catch((error) => {
logger.error("Error in sound preparation:", error);
});
} else if (settings.startupSound.enabled) {
logger.log(
`Startup sound skipped due to probability (${settings.startupSound?.probability}% chance)`,
);
}
},
stop() {
logger.log("Shiggy Enhancements: Stopping plugin");
// Stop and cleanup audio
if (DCDSoundManager && isPlaying) {
DCDSoundManager.stop(soundId);
isPlaying = false;
}
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
// Reset preparation state so it can play again next time
isPrepared = false;
},
});

View file

@ -22,7 +22,6 @@ import {
} from "@metro/common/components";
import { ErrorBoundary, Search } from "@ui/components";
import { isNotNil } from "es-toolkit";
import fuzzysort from "fuzzysort";
import {
ComponentType,
ReactNode,
@ -181,6 +180,27 @@ export default function AddonPage<T extends object>({
const { bottom: bottomInset } = useSafeAreaInsets();
const navigation = NavigationNative.useNavigation();
// Lazy-load fuzzysort to reduce initial bundle size
const fuzzysortRef = React.useRef<any>(null);
const [fuzzysortReady, setFuzzysortReady] = React.useState(false);
React.useEffect(() => {
let mounted = true;
import("fuzzysort")
.then((mod) => {
const f = (mod as any).default ?? mod;
if (mounted) {
fuzzysortRef.current = f;
setFuzzysortReady(true);
}
})
.catch(() => {
// ignore load errors
});
return () => {
mounted = false;
};
}, []);
useEffect(() => {
if (props.OptionsActionSheetComponent) {
navigation.setOptions({
@ -205,11 +225,13 @@ export default function AddonPage<T extends object>({
const items = values.filter((i) => isNotNil(i) && typeof i === "object");
if (!search && sortFn) items.sort(sortFn);
return fuzzysort.go(search, items, {
if (!fuzzysortReady || !fuzzysortRef.current) return [] as any[];
return fuzzysortRef.current.go(search, items, {
keys: props.searchKeywords,
all: true,
});
}, [props.items, sortFn, search]);
}, [props.items, sortFn, search, fuzzysortReady]);
const onInstallPress = useCallback(() => {
if (!props.installAction) return () => {};

View file

@ -11,10 +11,11 @@ import {
Text,
} from "@metro/common/components";
import { showSheet } from "@ui/sheets";
import chroma from "chroma-js";
import { createContext, useContext, useMemo } from "react";
import { Image, View } from "react-native";
const getChroma = () => require("chroma-js") as typeof import("chroma-js").default;
const CardContext = createContext<{
plugin: UnifiedPluginModel;
result: Fuzzysort.KeysResult<UnifiedPluginModel>;
@ -22,7 +23,7 @@ const CardContext = createContext<{
const useCardContext = () => useContext(CardContext);
function getHighlightColor(): import("react-native").ColorValue {
return chroma(tokens.unsafe_rawColors.YELLOW_300).alpha(0.3).hex();
return getChroma()(tokens.unsafe_rawColors.YELLOW_300).alpha(0.3).hex();
}
function Title() {

View file

@ -147,7 +147,12 @@ export const initVendettaObject = (): any => {
return require("moment");
},
get chroma() {
return require("chroma-js");
// Lazy-load chroma-js to reduce startup cost
const cached = (this as any)._chroma;
if (cached) return cached;
const loaded = require("chroma-js");
(this as any)._chroma = loaded;
return loaded;
},
get lodash() {
return require("lodash");

View file

@ -153,6 +153,8 @@ export interface StorageBackend {
}
const ILLEGAL_CHARS_REGEX = /[<>:"/\\|?*]/g;
// Debouncer map for per-store writes to disk
const _writeDebouncers = new Map<string, any>();
const filePathFixer = (file: string): string => Platform.select({
default: file,
@ -215,6 +217,71 @@ export const createMMKVBackend = (store: string, defaultData = {}) => {
})());
};
// Lightweight in-memory backend to accelerate startup and avoid blocking IO
// while still providing eventual persistence to the native storage.
export const createMemoryBackend = (
store: string,
defaultData: any = {},
) => {
const mmkvPath = getMMKVPath(store);
const defaultStr = JSON.stringify(defaultData);
// In-memory backing object used by the proxy immediately
const memory: any = { ...defaultData };
// Load persisted data in background without blocking startup
(async () => {
try {
const cached = await NativeCacheModule.getItem(store);
if (cached) {
const parsed = JSON.parse(cached);
Object.assign(memory, parsed);
}
} catch {
// ignore
}
try {
const path = `${NativeFileModule.getConstants().DocumentsDirPath}/${mmkvPath}`;
if (await NativeFileModule.fileExists(path)) {
const content = await NativeFileModule.readFile(path, "utf8");
const data = JSON.parse(content);
Object.assign(memory, data);
}
} catch {
// ignore
}
})();
// Hydration is intentionally omitted for stability in this rewrite
// Local path helper (inlined to avoid hoisting concerns)
const fixPath = (file: string) =>
Platform.select({ default: file, ios: NativeFileModule.saveFileToGallery ? file : `Documents/${file}` });
return {
get: async () => memory,
set: async (data: any) => {
Object.assign(memory, data);
// Debounced persistence to disk to avoid IO storms
try {
const existing = _writeDebouncers.get(store);
if (existing) clearTimeout(existing);
const timeout = setTimeout(async () => {
try {
await NativeFileModule.writeFile("documents", fixPath(mmkvPath), JSON.stringify(memory), "utf8");
} catch {
// ignore persistence errors to keep startup fast
}
}, 120);
_writeDebouncers.set(store, timeout);
} catch {
// ignore
}
},
} as unknown as StorageBackend;
};
export const createFileBackend = (file: string, defaultData = {}, migratePromise?: Promise<void>): StorageBackend => {
return {
get: async () => {
@ -239,4 +306,3 @@ export const createFileBackend = (file: string, defaultData = {}, migratePromise
}
};
};

View file

@ -1,4 +1,4 @@
import { awaitStorage, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
import { awaitStorage, createMemoryBackend, createStorage, wrapSync } from "@core/vendetta/storage";
import { clearFolder, downloadFile, fileExists, removeFile, writeFile } from "@lib/api/native/fs";
import { safeFetch } from "@lib/utils";
@ -13,7 +13,8 @@ export type FontDefinition = {
}
type FontStorage = Record<string, FontDefinition> & { __selected?: string; };
export const fonts = wrapSync(createStorage<FontStorage>(createMMKVBackend("BUNNY_FONTS")));
// Use a memory-backed storage for faster startup; persistence is still written back to disk
export const fonts = wrapSync(createStorage<FontStorage>(createMemoryBackend("BUNNY_FONTS", {})));
async function writeFont(font: FontDefinition | null) {
if (!font && font !== null) throw new Error("Arg font must be a valid object or null");

View file

@ -1,8 +1,9 @@
import { findByProps } from "@metro";
import chroma from "chroma-js";
import { omit } from "es-toolkit";
import { Platform, processColor } from "react-native";
const getChroma = () => require("chroma-js") as typeof import("chroma-js").default;
import { colorsPref } from "./preferences";
import { ColorManifest, InternalColorDefinition } from "./types";
@ -33,7 +34,7 @@ export function parseColorManifest(manifest: ColorManifest): InternalColorDefini
} else if (typeof semanticColorValue === "string") {
if (semanticColorValue.startsWith("#")) {
semanticColorDefinitions[semanticColorKey] = {
value: chroma(semanticColorValue).hex(),
value: getChroma()(semanticColorValue).hex(),
opacity: 1,
};
} else {
@ -208,7 +209,7 @@ export function applyAndroidAlphaKeys(rawColors?: Record<string, string>) {
for (const key in alphaMap) {
const [colorKey, alpha] = alphaMap[key];
if (!rawColors[colorKey]) continue;
rawColors[key] = chroma(rawColors[colorKey]).alpha(alpha).hex();
rawColors[key] = getChroma()(rawColors[colorKey]).alpha(alpha).hex();
}
return rawColors;
@ -221,11 +222,11 @@ export function normalizeToHex(colorString: string | undefined): string | undefi
return "#00000000";
}
if (chroma.valid(colorString)) return chroma(colorString).hex();
if (getChroma().valid(colorString)) return getChroma()(colorString).hex();
const color = Number(processColor(colorString));
return chroma.rgb(
return getChroma().rgb(
color >> 16 & 0xff, // red
color >> 8 & 0xff, // green
color & 0xff, // blue

View file

@ -4,7 +4,7 @@ import { after } from "@lib/api/patcher";
import { useObservable } from "@lib/api/storage";
import { findInReactTree } from "@lib/utils";
import { findByFilePathLazy } from "@metro";
import chroma from "chroma-js";
const getChroma = () => require("chroma-js") as typeof import("chroma-js").default;
import { ImageBackground, StyleSheet } from "react-native";
import { logger } from "@lib/utils/logger";
@ -43,7 +43,7 @@ export default function patchChatBackground() {
if (messagesComponent) {
const flattened = StyleSheet.flatten(messagesComponent.props.style);
const backgroundColor = chroma(
const backgroundColor = getChroma()(
flattened.backgroundColor || "black"
).alpha(
1 - (_colorRef.current.background?.opacity ?? 1)

View file

@ -4,7 +4,7 @@ import { before, instead } from "@lib/api/patcher";
import { findByProps } from "@metro";
import { byMutableProp } from "@metro/filters";
import { createLazyModule } from "@metro/lazy";
import chroma from "chroma-js";
const getChroma = () => require("chroma-js") as typeof import("chroma-js").default;
const tokenReference = findByProps("SemanticColor");
const isThemeModule = createLazyModule(byMutableProp("isThemeDark"));
@ -67,14 +67,14 @@ export default function patchDefinitionAndResolver() {
if (semanticDef?.value) {
if (semanticDef.opacity === 1) return semanticDef.value;
return chroma(semanticDef.value).alpha(semanticDef.opacity).hex();
return getChroma()(semanticDef.value).alpha(semanticDef.opacity).hex();
}
const rawKey = colorDef.raw;
const rawValue = rawKey ? currentRef.raw[rawKey] : undefined;
if (rawValue) {
// Set opacity if needed
return (colorDef.opacity === 1) ? rawValue : chroma(rawValue).alpha(colorDef.opacity).hex();
return (colorDef.opacity === 1) ? rawValue : getChroma()(rawValue).alpha(colorDef.opacity).hex();
}
// Fallback to default

View file

@ -1,6 +1,6 @@
import { Nullish } from "@lib/utils/types";
import { TextStyles, ThemeColors } from "@ui/types";
import { Falsey } from "lodash";
import type { Falsey } from "lodash";
import { FC, MutableRefObject, PropsWithoutRef, ReactNode, RefObject } from "react";
import type * as RN from "react-native";
import { ImageSourcePropType, PressableProps } from "react-native";