This commit is contained in:
OTKUSteyler 2026-07-31 23:34:58 +02:00
commit 27dc770a14
8 changed files with 546 additions and 1745 deletions

1967
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -13,45 +13,40 @@
},
"license": "BSD-3-Clause",
"devDependencies": {
"@saithodev/semantic-release-backmerge": "^4.0.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^7.1.0",
"@semantic-release/git": "^10.0.1",
"semantic-release": "^25.0.3",
"@swc/core": "^1.15.24",
"@swc/helpers": "^0.5.21",
"@types/node": "^25.5.2",
"@swc/core": "^1.15.47",
"@swc/helpers": "^0.5.23",
"@types/node": "^26.1.2",
"@types/yargs-parser": "^21.0.3",
"@typescript-eslint/eslint-plugin": "^8.58.0",
"@typescript-eslint/parser": "^8.58.0",
"@typescript-eslint/typescript-estree": "^8.58.0",
"chalk": "^5.6.2",
"esbuild": "^0.28.0",
"@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.65.0",
"@typescript-eslint/typescript-estree": "^8.65.0",
"chalk": "^6.0.0",
"esbuild": "^0.28.1",
"esbuild-plugin-globals": "^0.2.0",
"eslint": "^10.2.0",
"eslint": "^10.8.0",
"eslint-plugin-import-alias": "^1.2.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-simple-import-sort": "^14.0.0",
"eslint-plugin-unused-imports": "^4.4.1",
"typescript": "^6.0.2",
"typescript": "^7.0.2",
"yargs-parser": "^22.0.0"
},
"dependencies": {
"@gullerya/object-observer": "^6.1.4",
"@tanstack/react-query": "^5.96.2",
"@gullerya/object-observer": "^7.0.0",
"@tanstack/react-query": "^5.101.4",
"@types/chroma-js": "~3.1.2",
"@types/lodash": "~4.17.24",
"@types/react": "19.2.14",
"es-toolkit": "^1.45.1",
"@types/react": "19.2.17",
"es-toolkit": "^1.50.0",
"fuzzysort": "^3.1.0",
"intl-messageformat": "^11.2.0",
"intl-messageformat": "^11.2.12",
"moment": "2.30.1",
"react-devtools": "^7.0.1",
"react-native": "0.84.1",
"react-native-reanimated": "^4.3.0",
"react-native-webview": "^13.16.1",
"react-native": "0.86.2",
"react-native-reanimated": "^4.5.3",
"react-native-webview": "^14.0.1",
"spitroast": "^2.1.6",
"type-fest": "^5.5.0"
"type-fest": "^5.8.0"
},
"trustedDependencies": [
"@swc/core",

View file

@ -56,6 +56,8 @@ const config = {
"!ShiggyCord-deps-shim!": "./shims/depsModule.ts",
spitroast: "./node_modules/spitroast",
"react/jsx-runtime": "./shims/jsxRuntime",
"@gullerya/object-observer":
path.resolve("./node_modules/@gullerya/object-observer/dist/object-observer.min.js"),
},
plugins: [
globalPlugin({
@ -75,7 +77,7 @@ const config = {
constModules: {
globals: {
"bunny-build-info": {
version: `"1.4.1.4"`,
version: `"1.4.1.6"`,
},
},
},

View file

@ -23,52 +23,54 @@ const { showSimpleActionSheet } = lazyDestructure(() =>
// TODO: These styles work weirdly. Low DPI Has cramped text. Fix?
const useStyles = createStyles({
card: {
backgroundColor: semanticColors?.CARD_SECONDARY_BG,
borderRadius: 12,
overflow: "hidden",
},
header: {
padding: 0,
},
headerLeading: {
flexDirection: "column",
justifyContent: "center",
flexShrink: 1,
scale: 1.2,
},
headerTrailing: {
display: "flex",
flexDirection: "row",
gap: 15,
alignItems: "center",
},
headerLabel: {
...TextStyleSheet["heading-md/semibold"],
color: semanticColors.MOBILE_TEXT_HEADING_PRIMARY,
},
headerSubtitle: {
...TextStyleSheet["text-md/semibold"],
color: semanticColors.TEXT_MUTED,
},
descriptionLabel: {
...TextStyleSheet["text-md/semibold"],
color: semanticColors.TEXT_NORMAL,
},
actions: {
flexDirection: "row-reverse",
alignItems: "center",
gap: 5,
},
iconStyle: {
tintColor: semanticColors.INTERACTIVE_ICON_DEFAULT,
opacity: 0.2,
height: 64,
width: 64,
left: void 0,
right: "30%",
top: "-10%",
},
card: {
backgroundColor: semanticColors?.CARD_SECONDARY_BG,
borderRadius: 12,
overflow: "hidden"
},
header: {
padding: 0,
},
headerLeading: {
flex: 1,
flexDirection: "column",
justifyContent: "center",
scale: 1.2
},
headerTrailing: {
display: "flex",
flexDirection: "row",
gap: 15,
alignItems: "center"
},
headerLabel: {
...TextStyleSheet["heading-lg/semibold"],
},
headerLabelCompact: {
...TextStyleSheet["text-md/semibold"],
},
headerSubtitle: {
...TextStyleSheet["text-sm/semibold"],
color: semanticColors.TEXT_MUTED,
},
descriptionLabel: {
...TextStyleSheet["text-md/semibold"],
color: "text-strong",
},
actions: {
flexDirection: "row-reverse",
alignItems: "center",
gap: 5
},
iconStyle: {
tintColor: semanticColors.LOGO_PRIMARY,
opacity: 0.2,
height: 64,
width: 64,
left: void 0,
right: "30%",
top: "-10%"
}
});
interface Action {

View file

@ -11,13 +11,8 @@ import { injectFluxInterceptor } from "@lib/api/flux";
import { patchJsx } from "@lib/api/react/jsx";
import { logger } from "@lib/utils/logger";
import { patchSettings } from "@ui/settings";
import { updaterSettings } from "@lib/api/settings";
import { InteractionManager } from "react-native";
import { getDebugInfo, initDebugger } from "@lib/api/debug";
// Debug toggle helper (temporary runtime fallback). The helper is dynamically
// imported when needed (to avoid bundling it permanently) and removed after use.
import * as lib from "./lib";
import { timings } from "@lib/utils/timings";
@ -66,52 +61,19 @@ export default async () => {
const { VdPluginManager } = await import("@core/vendetta/plugins");
const { initPlugins, updatePlugins } = await import("@lib/addons/plugins");
// Initialize Vendetta plugins (may start many plugins) — do not block UI.
VdPluginManager.initPlugins()
.then((u) => lib.unload.push(u))
.catch((e) => logger.log("Vendetta init failed:", e));
await Promise.all([
VdPluginManager.initPlugins()
.then((u) => u && lib.unload.push(u))
.catch((e) => logger.log("Vendetta init failed:", e)),
initPlugins()
.catch((e) => logger.log("initPlugins failed:", e)),
]);
// Start ShiggyCord (Bunny) plugins now without forcing repository updates.
// Plugin repository fetching is deferred so the app can finish launching first.
// Stagger plugin startup to reduce CPU/memory spikes: use smaller batches and a small interval.
// This keeps the UI responsive while plugins initialize in the background.
initPlugins({ staggerInterval: 500, batchSize: 2 });
// Attempt a lightweight recovery toggle if some core plugins failed to start.
try {
// Dynamically import the helper (if present) but suppress verbose errors.
const mod = await import("@core/debug/toggleCorePlugins").catch(
() => null,
);
if (mod?.default) {
// Run helper with minimal noise; ignore failures.
mod.default({ offDuration: 1500 }).catch(() => {});
}
// Try to remove the helper source file (best-effort, ignore failures).
await import("@lib/api/native/fs")
.then((fs) => fs.removeFile("src/core/debug/toggleCorePlugins.ts"))
.catch(() => {});
} catch {
// suppressed
}
updatePlugins()
.catch((e) => logger.log("updatePlugins failed:", e));
// Update fonts in background
updateFonts().catch((e) => logger.log("updateFonts failed:", e));
// Schedule plugin repository update after a delay (5 minutes) so update work
// does not impact initial launch performance.
setTimeout(
() => {
updatePlugins().catch((e) =>
logger.log("updatePlugins failed (deferred 5min):", e),
);
},
5 * 60 * 1000,
);
// Note: we intentionally moved the call to `updatePlugins()` above so core
// plugins are registered prior to calling `initPlugins()`.
};
// Preferred: wait until interactions finish (animations / navigation).

View file

@ -752,27 +752,54 @@ export async function updatePlugins() {
}),
);
}
const enabledIds = [...registeredPlugins.keys()].filter(
(id) => isPluginEnabled(id) && !pluginInstances.has(id),
);
await Promise.allSettled(
enabledIds.map(async (id) => {
try {
await startPlugin(id).catch((e) => {
console.error(`Failed to start plugin ${id}:`, e);
});
} catch (error) {
console.error(`Unexpected error while starting plugin ${id}:`, error);
}
}),
);
}
/**
* Initialize plugins but stagger startup to avoid CPU spikes during app start.
* Register all installed plugins from local storage manifests without any
* network fetch. This lets initPlugins() start plugins immediately using
* previously-cached scripts, keeping startup snappy.
*
* This replaces the original "start all enabled plugins in parallel" approach
* with a configurable staggered startup: plugins are started in small batches and
* we await a small interval between batches. This reduces short-lived CPU and I/O contention
* that causes jank on low-end devices.
*
* Backwards-compatible: callers can still call initPlugins() with no arguments.
*
* Options:
* - staggerInterval (ms) - wait time between batches (default: 100)
* - batchSize - how many plugins to start concurrently in a single batch (default: 3)
* Safe to call multiple times — skips already-registered plugins.
*/
export async function initPlugins(
options: { staggerInterval?: number; batchSize?: number } = {},
) {
const { staggerInterval = 100, batchSize = 3 } = options;
export async function registerInstalledPlugins() {
await awaitStorage(pluginRepositories, pluginSettings);
const ids = Object.keys(pluginSettings ?? {});
if (ids.length === 0) return;
// Load all manifest files from local storage in parallel.
const results = await Promise.allSettled(
ids.map(async (id) => {
if (registeredPlugins.has(id) || isCorePlugin(id)) return;
const loaded = await preloadStorageIfExists(`plugins/manifests/${id}.json`);
if (loaded) {
const manifest = getPreloadedStorage<t.BunnyPluginManifest>(
`plugins/manifests/${id}.json`,
);
if (manifest) {
registeredPlugins.set(id, manifest);
}
}
}),
);
}
export async function initPlugins() {
// Ensure core plugins are registered prior to any plugin startup logic so
// settings pages and UI that read registered/core plugins show them.
// registerCorePlugins is synchronous and idempotent, but call it here as a
@ -784,6 +811,7 @@ export async function initPlugins(
}
await awaitStorage(pluginRepositories, pluginSettings);
await registerInstalledPlugins();
// Collect enabled plugin ids
const enabledIds = [...registeredPlugins.keys()].filter((id) =>
@ -792,32 +820,18 @@ export async function initPlugins(
if (enabledIds.length === 0) return;
// Helper sleep
const sleep = (ms: number) => new Promise<void>((res) => setTimeout(res, ms));
// Start plugins in batches to avoid starting all at once.
for (let i = 0; i < enabledIds.length; i += batchSize) {
const batch = enabledIds.slice(i, i + batchSize);
// Start the batch in parallel but don't let exceptions bubble to halt everything.
await Promise.allSettled(
batch.map(async (id) => {
try {
// Only attempt to start if still enabled and not already started
if (!isPluginEnabled(id) || pluginInstances.has(id)) return;
await startPlugin(id).catch((e) => {
// startPlugin may throw; ensure it doesn't crash the whole batch
console.error(`Failed to start plugin ${id}:`, e);
});
} catch (error) {
console.error(`Unexpected error while starting plugin ${id}:`, error);
}
}),
);
// If there are more plugins to start, wait before starting the next batch.
if (i + batchSize < enabledIds.length) {
await sleep(staggerInterval);
}
}
// Start all enabled plugins in parallel
await Promise.allSettled(
enabledIds.map(async (id) => {
try {
// Only attempt to start if still enabled and not already started
if (!isPluginEnabled(id) || pluginInstances.has(id)) return;
await startPlugin(id).catch((e) => {
console.error(`Failed to start plugin ${id}:`, e);
});
} catch (error) {
console.error(`Unexpected error while starting plugin ${id}:`, error);
}
}),
);
}

View file

@ -141,7 +141,7 @@ export function getCurrentTheme() {
* @internal
*/
export function getThemeFromLoader(): VdThemeInfo | null {
return getStoredTheme();
return getStoredTheme() ?? getCurrentTheme();
}
/**
@ -157,6 +157,7 @@ export async function initThemes() {
}
await newAwaitStorage(colorsPref);
await awaitStorage(themes);
const currentTheme = getThemeFromLoader();
initColors(currentTheme?.data ?? null);

View file

@ -10,6 +10,9 @@ const rainLoaderIdentity = globalThis.__RAIN_LOADER__;
// @ts-ignore
const vendettaLoaderIdentity = globalThis.__vendetta_loader;
// @ts-ignore
const shiggyLoaderIdentity = globalThis.__SHIGGY_LOADER__;
export interface VendettaLoaderIdentity {
name: string;
features: {
@ -36,6 +39,10 @@ export function isRa1nLoader() {
return rainLoaderIdentity != null;
}
export function isShiggyLoader() {
return shiggyLoaderIdentity != null;
}
function polyfillVendettaLoaderIdentity() {
if (!isPyonLoader() || isVendettaLoader() || !isRa1nLoader()) return null;
@ -98,6 +105,8 @@ export function getLoaderIdentity() {
return getVendettaLoaderIdentity();
} else if (isRa1nLoader()) {
return rainLoaderIdentity();
} else if (isShiggyLoader()) {
return shiggyLoaderIdentity;
}
return null;
@ -116,6 +125,7 @@ export function getLoaderName() {
if (isPyonLoader()) return pyonLoaderIdentity.loaderName;
else if (isRa1nLoader()) return rainLoaderIdentity.loadername;
else if (isVendettaLoader()) return vendettaLoaderIdentity.name;
else if (isShiggyLoader()) return shiggyLoaderIdentity.loaderName;
return "Unknown";
}
@ -123,6 +133,7 @@ export function getLoaderName() {
export function getLoaderVersion(): string | null {
if (isPyonLoader()) return pyonLoaderIdentity.loaderVersion;
else if (isRa1nLoader()) return rainLoaderIdentity.loaderVersion;
else if (isShiggyLoader()) return shiggyLoaderIdentity.loaderVersion;
return null;
}
@ -144,7 +155,9 @@ export function isThemeSupported() {
} else if (isVendettaLoader()) {
return vendettaLoaderIdentity!!.features.themes != null;
} else if (isRa1nLoader()) {
return false; // Ra1n has theme support disabled, this is here just to make sure it doesnt think it does
return false; // Rain has theme support disabled, this is here just to make sure it doesnt think it does
} else if (isShiggyLoader()) {
return shiggyLoaderIdentity.hasThemeSupport;
}
return false;
@ -158,6 +171,8 @@ export function getStoredTheme(): VdThemeInfo | null {
if (!themeProp) return null;
// @ts-ignore
return globalThis[themeProp] || null;
} else if (isShiggyLoader()) {
return shiggyLoaderIdentity.storedTheme || null;
}
return null;
@ -168,6 +183,8 @@ export function getThemeFilePath() {
return "pyoncord/current-theme.json";
} else if (isVendettaLoader()) {
return "vendetta_theme.json";
} else if (isShiggyLoader()) {
return "ShiggyCord/current-theme.json";
}
return null;
@ -241,6 +258,7 @@ export function getLoaderConfigPath() {
export function isFontSupported() {
if (isPyonLoader()) return pyonLoaderIdentity.fontPatch === 2;
else if (isShiggyLoader()) return shiggyLoaderIdentity.fontPatch === 2;
return false;
}
@ -248,4 +266,4 @@ export function isFontSupported() {
export async function clearBundle() {
// TODO: This should be not be hardcoded, maybe put in loader.json?
return void await removeCacheFile("bundle.js");
}
}