Add Message Logger and ShiggyHelper core plugins, improve plugin listing

and settings UI

- Added Message Logger core plugin (port of Antied) - Added ShiggyHelper
core plugin with verbose logging and log dump UI - Improved /plugins
command to show core and external plugins separately - Split core and
external plugins into separate settings pages - Updated theme selection
logic for better color sync - Updated MessageFix author attribution -
Bumped version to 1.1.0
This commit is contained in:
kmmiio99o 2025-10-12 17:46:51 +02:00
parent 3f01f858e9
commit 6b7ec92149
16 changed files with 889 additions and 116 deletions

View file

@ -74,7 +74,7 @@ const config = {
constModules: {
globals: {
"bunny-build-info": {
version: `"1.1.0-3"`,
version: `"1.1.0"`,
},
},
},

View file

@ -1,43 +1,199 @@
import { Strings } from "@core/i18n";
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
import { ApplicationCommand, ApplicationCommandOptionType } from "@lib/api/commands/types";
import {
ApplicationCommand,
ApplicationCommandOptionType,
} from "@lib/api/commands/types";
import { messageUtil } from "@metro/common";
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
import {
registeredPlugins,
corePluginInstances,
pluginSettings,
} from "@lib/addons/plugins";
export default () => <ApplicationCommand>{
/**
* /plugins
*
* Aggregates:
* - Core plugins (built-in) from corePluginInstances
* - External plugins:
* - Vendetta-managed plugins (VdPluginManager.plugins)
* - Bunny repository plugins (registeredPlugins + pluginSettings)
*
* Dedupes by plugin id and shows Enabled / Disabled groups.
* For external plugins each entry will show its source(s) (vendetta / bunny).
*/
export default () =>
<ApplicationCommand>{
name: "plugins",
description: Strings.COMMAND_PLUGINS_DESC,
options: [
{
name: "ephemeral",
displayName: "ephemeral",
type: ApplicationCommandOptionType.BOOLEAN,
description: Strings.COMMAND_DEBUG_OPT_EPHEMERALLY,
}
{
name: "ephemeral",
displayName: "ephemeral",
type: ApplicationCommandOptionType.BOOLEAN,
description: Strings.COMMAND_DEBUG_OPT_EPHEMERALLY,
},
],
execute([ephemeral], ctx) {
const plugins = Object.values(VdPluginManager.plugins).filter(Boolean) as unknown as VendettaPlugin[];
plugins.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
// --- Core plugins ---
const coreIds = [...corePluginInstances.keys()];
const enabled = plugins.filter(p => p.enabled).map(p => p.manifest.name);
const disabled = plugins.filter(p => !p.enabled).map(p => p.manifest.name);
const coreManifests = coreIds
.map((id) => registeredPlugins.get(id))
.filter(Boolean) as any[];
const content = [
`**Installed Plugins (${plugins.length}):**`,
...(enabled.length > 0 ? [
`Enabled (${enabled.length}):`,
"> " + enabled.join(", "),
] : []),
...(disabled.length > 0 ? [
`Disabled (${disabled.length}):`,
"> " + disabled.join(", "),
] : []),
].join("\n");
const coreEnabled = coreManifests
.filter((m) => Boolean(pluginSettings[m.id]?.enabled))
.map((m) => m.display?.name ?? m.name ?? m.id)
.sort((a, b) => a.localeCompare(b));
const coreDisabled = coreManifests
.filter((m) => !Boolean(pluginSettings[m.id]?.enabled))
.map((m) => m.display?.name ?? m.name ?? m.id)
.sort((a, b) => a.localeCompare(b));
if (ephemeral?.value) {
messageUtil.sendBotMessage(ctx.channel.id, content);
} else {
const fixNonce = Date.now().toString();
messageUtil.sendMessage(ctx.channel.id, { content }, void 0, {nonce:fixNonce});
// --- External plugins aggregation (Vendetta + Bunny) ---
type ExtEntry = {
id: string;
name: string;
sources: Set<string>;
enabled: boolean; // overall enabled if any source reports enabled (Vendetta) or pluginSettings for Bunny
};
const aggregated = new Map<string, ExtEntry>();
// Vendetta plugins (if available)
try {
const vdRaw =
(VdPluginManager && (VdPluginManager as any).plugins) || {};
const vdPlugins = Object.values(vdRaw).filter(
Boolean,
) as VendettaPlugin[];
for (const p of vdPlugins) {
const id =
p.manifest?.id ?? p.id ?? String(p.manifest?.name ?? Math.random());
const name = p.manifest?.name ?? id;
const enabled = Boolean(p.enabled);
const existing = aggregated.get(id);
if (existing) {
existing.sources.add("vendetta");
existing.enabled = existing.enabled || enabled;
} else {
aggregated.set(id, {
id,
name,
sources: new Set(["vendetta"]),
enabled,
});
}
}
}
};
} catch (e) {
// Defensive: if Vendetta manager isn't ready, skip vendetta aggregation
}
// Bunny external plugins: use registeredPlugins + pluginSettings (installed)
for (const [id, manifest] of registeredPlugins) {
if (!manifest) continue;
// External bunny plugins have parentRepository property
// Only consider those that are actual external plugins (have parentRepository)
const isExternal = (manifest as any).parentRepository != null;
if (!isExternal) continue;
// Skip core plugin ids (safety)
if (corePluginInstances.has(id)) continue;
const name = manifest.display?.name ?? manifest.name ?? id;
const installed = pluginSettings[id] != null;
const enabled = Boolean(pluginSettings[id]?.enabled);
// If not installed and not present in aggregated (vendetta), still include as available but mark enabled=false
const existing = aggregated.get(id);
if (existing) {
existing.sources.add("bunny");
existing.enabled = existing.enabled || enabled;
} else {
// Only add bunny entry if installed OR if it's registered and not installed we will handle as available later
if (installed) {
aggregated.set(id, {
id,
name,
sources: new Set(["bunny"]),
enabled,
});
}
}
}
// Also collect available-but-not-installed Bunny externals for the "available" list
const available = [...registeredPlugins.values()]
.filter(
(m: any) =>
Boolean(m) &&
(m as any).parentRepository &&
!corePluginInstances.has((m as any).id),
)
.map((m: any) => ({
id: m.id,
name: m.display?.name ?? m.name ?? m.id,
installed: pluginSettings[m.id] != null,
}))
.filter((x) => !x.installed)
.sort((a, b) => a.name.localeCompare(b.name))
.map((x) => x.name);
// Build external lists (deduped)
const externalList = Array.from(aggregated.values()).sort((a, b) =>
a.name.localeCompare(b.name),
);
const externalEnabled = externalList
.filter((e) => e.enabled)
.map((e) => e.name);
const externalDisabled = externalList
.filter((e) => !e.enabled)
.map((e) => e.name);
// Build message
const lines: string[] = [];
// Core plugins
lines.push(`**Plugins (${coreManifests.length}):**`);
if (coreEnabled.length > 0) {
lines.push(`Enabled (${coreEnabled.length}):`);
lines.push("> " + coreEnabled.join(", "));
}
if (coreDisabled.length > 0) {
lines.push(`Disabled (${coreDisabled.length}):`);
lines.push("> " + coreDisabled.join(", "));
}
if (coreManifests.length === 0) {
lines.push("_No core plugins registered._");
}
// External plugins
lines.push(`**External Plugins (${externalList.length}):**`);
if (externalEnabled.length > 0) {
lines.push(`Installed & Enabled (${externalEnabled.length}):`);
lines.push("> " + externalEnabled.join(", "));
}
if (externalDisabled.length > 0) {
lines.push(`Installed & Disabled (${externalDisabled.length}):`);
lines.push("> " + externalDisabled.join(", "));
}
if (externalList.length === 0) {
lines.push("_No external plugins found._");
}
const content = lines.join("\n");
if (ephemeral?.value) {
messageUtil.sendBotMessage(ctx.channel.id, content);
} else {
const fixNonce = Date.now().toString();
messageUtil.sendMessage(ctx.channel.id, { content }, void 0, {
nonce: fixNonce,
});
}
},
};

View file

@ -5,88 +5,90 @@ import { useEffect, useState } from "react";
import { defineCorePlugin } from "..";
interface BunnyBadge {
label: string;
url: string;
label: string;
url: string;
}
const useBadgesModule = findByName("useBadges", false);
export default defineCorePlugin({
manifest: {
id: "bunny.badges",
version: "1.1.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "Badges",
description: "Adds badges to user's profile",
authors: [{ name: "cocobo1"}, { name: "pylixonly" }]
}
manifest: {
id: "bunny.badges",
version: "1.1.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "Badges",
description: "Adds badges to user's profile",
authors: [{ name: "cocobo1" }, { name: "pylixonly" }],
},
start() {
let allBadges: { [x: string]: any; } | null = null;
const badgeProps = {} as Record<string, any>;
onJsxCreate("ProfileBadge", (component, ret) => {
if (ret.props.id?.startsWith("bunny-")) {
const cachedProps = badgeProps[ret.props.id];
if (cachedProps) {
ret.props.source = cachedProps.source;
ret.props.label = cachedProps.label;
ret.props.id = cachedProps.id;
}
}
});
onJsxCreate("RenderedBadge", (component, ret) => {
if (ret.props.id?.startsWith("bunny-")) {
const cachedProps = badgeProps[ret.props.id];
if (cachedProps) {
Object.assign(ret.props, cachedProps);
}
}
});
after("default", useBadgesModule, ([user], result) => {
const [badges, setBadges] = useState<BunnyBadge[]>([]);
useEffect(() => {
if (!user) return;
if (!allBadges) {
fetch("https://codeberg.org/raincord/badges/raw/branch/main/badges.json")
.then(r => r.json())
.then(data => {
allBadges = data;
//@ts-expect-error
setBadges(allBadges[user.userId] || []);
})
} else {
//user has no badges, but maybe they should get some by contributing
setBadges(allBadges[user.userId] || []);
}
}, [user?.userId]);
if (user && badges.length > 0) {
badges.forEach((badge, i) => {
const badgeId = `bunny-${user.userId}-${i}`;
badgeProps[badgeId] = {
id: badgeId,
source: { uri: badge.url },
label: badge.label,
userId: user.userId,
};
},
result.push({
id: badgeId,
description: badge.label,
icon: " _",
});
});
}
start() {
let allBadges: { [x: string]: any } | null = null;
const badgeProps = {} as Record<string, any>;
onJsxCreate("ProfileBadge", (component, ret) => {
if (ret.props.id?.startsWith("bunny-")) {
const cachedProps = badgeProps[ret.props.id];
if (cachedProps) {
ret.props.source = cachedProps.source;
ret.props.label = cachedProps.label;
ret.props.id = cachedProps.id;
}
}
});
onJsxCreate("RenderedBadge", (component, ret) => {
if (ret.props.id?.startsWith("bunny-")) {
const cachedProps = badgeProps[ret.props.id];
if (cachedProps) {
Object.assign(ret.props, cachedProps);
}
}
});
after("default", useBadgesModule, ([user], result) => {
const [badges, setBadges] = useState<BunnyBadge[]>([]);
useEffect(() => {
if (!user) return;
if (!allBadges) {
fetch(
"https://codeberg.org/raincord/badges/raw/branch/main/badges.json",
)
.then((r) => r.json())
.then((data) => {
allBadges = data;
//@ts-expect-error
setBadges(allBadges[user.userId] || []);
});
} else {
//user has no badges, but maybe they should get some by contributing
setBadges(allBadges[user.userId] || []);
}
}, [user?.userId]);
if (user && badges.length > 0) {
badges.forEach((badge, i) => {
const badgeId = `bunny-${user.userId}-${i}`;
badgeProps[badgeId] = {
id: badgeId,
source: { uri: badge.url },
label: badge.label,
userId: user.userId,
};
result.push({
id: badgeId,
description: badge.label,
icon: " _",
});
});
}
});
}
});
},
});

View file

@ -11,7 +11,9 @@ export const getCorePlugins = (): Record<string, CorePlugin> => ({
"bunny.badges": require("./badges"),
"bunny.notrack": { ...require("./notrack"), preenabled: true },
"bunny.messagefix": { ...require("./messagefix") },
"bunny.messagelogger": { ...require("./messagelogger"), preenabled: true },
"bunny.fixembed": { ...require("./fixembed"), preenabled: true },
"bunny.shiggyhelper": { ...require("./shiggyhelper") },
});
/**

View file

@ -16,7 +16,7 @@ export default defineCorePlugin({
display: {
name: "MessageFix",
description: "Ensures messages include the required nonce parameter",
authors: [{ name: "ShiggyCord Team" }],
authors: [{ name: "Win8.1VMUser (plugin by ShiggyCord Team)" }],
},
},

View file

@ -0,0 +1,43 @@
import { defineCorePlugin } from "..";
import fluxDispatchPatch from "./patches/flux_dispatch";
import selfEditPatch from "./patches/self_edit";
import actionsheet from "./patches/actionsheet";
export const regexEscaper = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
export let isEnabled = false;
const deletedMessageArray = new Map();
let patches = [];
export default defineCorePlugin({
manifest: {
id: "bunny.messagelogger",
version: "1.0.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "Message Logger",
description: "Port of the popular Revenge/Kettu plugin called Antied.",
authors: [{ name: "ShiggyCord Team, Angelw0lf" }],
},
},
start() {
isEnabled = true;
// Each patch function is expected to return an unpatch function.
patches = [
fluxDispatchPatch(deletedMessageArray),
actionsheet(),
selfEditPatch(),
].filter(Boolean);
},
stop() {
isEnabled = false;
patches.forEach((p) => p && p());
patches = [];
},
});

View file

@ -0,0 +1,139 @@
import { before, after } from "@lib/api/patcher";
import { findAssetId } from "@lib/api/assets";
import { findInReactTree } from "@lib/utils";
import { FluxDispatcher, React } from "@metro/common";
import { showToast } from "@lib/ui/toasts";
import { findByProps } from "@metro";
import { regexEscaper, isEnabled } from "..";
const ActionSheet = findByProps("openLazy", "hideActionSheet");
const MessageStore = findByProps("getMessage", "getMessages");
const ChannelStore = findByProps("getChannel", "getDMFromUserId");
const ChannelMessages = findByProps("_channelMessages");
const { ActionSheetRow } = findByProps("ActionSheetRow");
/** @param {any} a */
function someFunc(a) {
// return a?.props?.label == i18n?.Messages?.MESSAGE_ACTION_REPLY
return a?.props?.label?.toLowerCase?.() == "reply";
}
export default () =>
before("openLazy", ActionSheet, ([component, args, actionMessage]) => {
if (isEnabled) {
try {
const message = actionMessage?.message;
if (args !== "MessageLongPressActionSheet" || !message) return;
component.then((/** @type {any} */ instance) => {
const unpatch = after("default", instance, (_, comp) => {
try {
React.useEffect(
() => () => {
unpatch();
},
[],
);
const buttons = findInReactTree(comp, (c) => c?.find?.(someFunc));
if (!buttons) return comp;
const position = Math.max(
buttons.findIndex(someFunc),
buttons.length - 1,
);
let originalMessage = null;
if (message?.channel_id && message?.id) {
originalMessage = MessageStore.getMessage(
message?.channel_id,
message?.id,
);
if (!originalMessage) {
const channel = ChannelMessages.get(message?.channel_id);
originalMessage = channel?.get(message?.id);
}
}
if (!originalMessage) return comp;
const escapedBuffer = regexEscaper("`[ EDITED ]`\n\n");
const separator = new RegExp(escapedBuffer, "gmi");
const checkIfBufferExist = separator.test(message.content);
if (checkIfBufferExist) {
const targetPos = position || 1;
const actionRow = React.createElement(
ActionSheetRow,
{
label: "Remove Edit History",
subLabel: "Added by Message Logger",
icon: React.createElement(ActionSheetRow.Icon, {
source: findAssetId("ic_edit_24px"),
}),
onPress: () => {
try {
const lats = message?.content?.split(separator);
const targetMessage = lats[lats.length - 1];
FluxDispatcher.dispatch({
type: "MESSAGE_UPDATE",
message: {
...message,
message_reference:
message?.message_reference ||
message?.messageReference ||
null,
content: `${targetMessage}`,
guild_id: ChannelStore.getChannel(
originalMessage.channel_id,
).guild_id,
},
otherPluginBypass: true,
});
ActionSheet.hideActionSheet();
showToast(
"History Removed",
findAssetId("ic_edit_24px"),
);
} catch (ex) {
showToast(
"[Message Logger] Failed to remove edit history",
);
console.error(
"[Message Logger] Error while removing history",
ex,
);
}
},
},
null,
);
buttons.splice(targetPos, 0, actionRow);
}
} catch (e) {
showToast(
"[Message Logger] Crash on ActionSheet, check debug log for more info",
);
console.error(
"[Message Logger] Error > ActionSheet:Component Patch\n",
e,
);
}
});
});
} catch (e) {
showToast(
"[Message Logger] Crash on ActionSheet, check debug log for more info",
);
console.error("[Message Logger] Error > ActionSheet Patch\n", e);
}
}
});

View file

@ -0,0 +1,125 @@
import { before } from "@lib/api/patcher";
import { findByProps } from "@metro";
import { showToast } from "@lib/ui/toasts";
import { FluxDispatcher } from "@metro/common";
import { isEnabled } from "..";
const ChannelStore = findByProps("getChannel", "getDMFromUserId");
const ChannelMessages = findByProps("_channelMessages");
const MessageStore = findByProps("getMessage", "getMessages");
export default (deletedMessageArray) =>
before("dispatch", FluxDispatcher, (args) => {
if (isEnabled) {
try {
const ev = args[0];
if (!ev || !ev.type) return;
/* =========================================================
MESSAGE_DELETE
==========================================================*/
if (ev.type === "MESSAGE_DELETE") {
if (ev.otherPluginBypass) return;
const orig = ChannelMessages.get(ev.channelId)?.get(ev.id);
if (!orig?.author?.id || !orig.author.username) return;
// ephemeral message dismiss (from bots)
if ((orig?.author?.bot && orig?.flags == 64) || orig.author.bot)
return;
// empty message check
if (
!orig.content &&
!orig.attachments?.length &&
!orig.embeds?.length
)
return;
const entry = deletedMessageArray.get(ev.id);
if (entry?.stage === 2) {
if (deletedMessageArray.size >= 100) {
deletedMessageArray = new Map();
}
return; // Kill message normally
}
if (entry?.stage === 1) {
// first pass
entry.stage = 2;
return entry.message || args;
}
const guildId = ChannelStore.getChannel(
orig.channel_id || ev.channelId,
)?.guild_id;
/* reuse the same object shape every time */
ev.message = {
...orig,
content: orig.content,
channel_id: orig.channel_id || ev.channelId,
guild_id: guildId,
message_reference:
orig?.message_reference || orig?.messageReference || null,
flags: 64,
};
ev.type = "MESSAGE_UPDATE";
ev.channelId = orig.channel_id || ev.channelId;
ev.optimistic = false;
ev.sendMessageOptions = {};
ev.isPushNotification = false;
deletedMessageArray.set(ev.id, { message: args, stage: 1 });
return args;
}
/* =========================================================
MESSAGE_UPDATE
==========================================================*/
if (ev.type === "MESSAGE_UPDATE") {
if (ev.otherPluginBypass) return;
const msg = ev.message;
if (!msg || msg.author?.bot) return;
const chId = msg.channel_id || ev.channelId;
const id = msg.id || ev.id;
const orig =
MessageStore.getMessage(chId, id) ||
ChannelMessages.get(chId)?.get(id);
if (!orig?.author?.id || !orig.author.username) return;
if (
!orig.content &&
!orig.attachments?.length &&
!orig.embeds?.length
)
return;
if (!msg.content || msg.content === orig.content) return;
let prefix = "`[ EDITED ]`\n\n";
ev.message = {
...msg,
content: `${orig.content} ${prefix}${msg.content}`,
guild_id: ChannelStore.getChannel(chId)?.guild_id ?? msg.guild_id,
edited_timestamp: "invalid_timestamp",
message_reference:
msg?.message_reference || orig?.messageReference || null,
};
return args;
}
} catch (e) {
showToast("[Message Logger] FluxDispatcher crash – check logs");
console.error("[Message Logger] Flux patch\n", e);
}
}
});

View file

@ -0,0 +1,21 @@
import { before } from "@lib/api/patcher";
import { findByProps } from "@metro";
import { regexEscaper, isEnabled } from "..";
const Message = findByProps("sendMessage", "startEditMessage");
export default () =>
before("startEditMessage", Message, (args) => {
if (!isEnabled) return;
const escaped = regexEscaper("`[ EDITED ]`\n\n");
const regexPattern = new RegExp(escaped, "gmi");
const [, , msg] = args;
if (typeof msg !== "string") return;
const parts = msg.split(regexPattern);
const final = parts[parts.length - 1] ?? "";
args[2] = final;
});

View file

@ -0,0 +1,226 @@
import { defineCorePlugin } from "..";
import { findByProps } from "@metro";
import { logger } from "@lib/utils/logger";
import { settings } from "@lib/api/settings";
import { React } from "@metro/common";
import { showToast } from "@lib/ui/toasts";
import { ScrollView } from "react-native";
// Button imported from components above
declare module "@lib/api/settings" {
interface Settings {
shiggyhelper?: {
verboseLogging?: boolean;
};
}
}
let originalConsoleLog: typeof console.log | null = null;
let originalConsoleError: typeof console.error | null = null;
let isPatched = false;
const capturedLogs: string[] = [];
function applyVerbosePatch(enabled: boolean) {
if (enabled && !isPatched) {
originalConsoleLog = console.log;
originalConsoleError = console.error;
console.log = (...args: any[]) => {
try {
const msg = args
.map((a) => (typeof a === "string" ? a : JSON.stringify(a)))
.join(" ");
capturedLogs.push(`[LOG ${new Date().toISOString()}] ${msg}`);
} catch {
// ignore stringify errors
capturedLogs.push(`[LOG ${new Date().toISOString()}] (unserializable)`);
}
originalConsoleLog?.apply(console, args);
};
console.error = (...args: any[]) => {
try {
const msg = args
.map((a) => (typeof a === "string" ? a : JSON.stringify(a)))
.join(" ");
capturedLogs.push(`[ERROR ${new Date().toISOString()}] ${msg}`);
} catch {
capturedLogs.push(
`[ERROR ${new Date().toISOString()}] (unserializable)`,
);
}
originalConsoleError?.apply(console, args);
};
isPatched = true;
logger.log("ShiggyHelper: Verbose logging enabled");
} else if (!enabled && isPatched) {
if (originalConsoleLog) console.log = originalConsoleLog;
if (originalConsoleError) console.error = originalConsoleError;
originalConsoleLog = null;
originalConsoleError = null;
isPatched = false;
logger.log("ShiggyHelper: Verbose logging disabled");
}
}
export default defineCorePlugin({
manifest: {
id: "bunny.shiggyhelper",
version: "1.0.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "ShiggyHelper",
description:
"Just a ShiggyHelper, because why not if you can have good logging?",
authors: [{ name: "ShiggyCord Team" }],
},
},
/**
* Simple settings UI exposing a toggle and some buttons for helper actions.
* Uses the project's Forms components via findByProps.
*/
SettingsComponent() {
const { useState, useEffect } = React;
// Use table-style rows (TableRowGroup / TableSwitchRow) for settings UI.
// Resolve UI row components; guard if unavailable.
const { TableRow, TableRowGroup, TableSwitchRow } = findByProps(
"TableRow",
"TableRowGroup",
"TableSwitchRow",
);
if (!TableRow || !TableRowGroup || !TableSwitchRow) {
return React.createElement(
ScrollView,
{ style: { flex: 1, padding: 12 } },
React.createElement(
Text,
null,
"ShiggyHelper UI unavailable (missing TableRow components).",
),
);
}
const [verbose, setVerbose] = useState<boolean>(
settings.shiggyhelper?.verboseLogging ?? false,
);
useEffect(() => {
// persist and apply
settings.shiggyhelper = settings.shiggyhelper || {};
settings.shiggyhelper.verboseLogging = verbose;
applyVerbosePatch(Boolean(verbose));
}, [verbose]);
const dumpLogs = () => {
if (!capturedLogs.length) {
showToast("No logs captured");
return;
}
const toShow = capturedLogs.slice(-10).join("\n");
logger.log("ShiggyHelper: Log dump (last 10):\n" + toShow);
showToast("Dumped last 10 logs to debug log");
};
const clearLogs = () => {
capturedLogs.length = 0;
showToast("Captured logs cleared");
};
const triggerTestLog = () => {
console.log("ShiggyHelper test log at", new Date().toISOString());
console.error("ShiggyHelper test error at", new Date().toISOString());
showToast("Test logs emitted");
};
// Actions should be disabled while verbose logging is off
const actionsDisabled = !verbose;
return React.createElement(ScrollView, { style: { flex: 1 } }, [
React.createElement(
TableRowGroup,
{ title: "ShiggyHelper" },
React.createElement(TableSwitchRow, {
label: "Verbose Logging",
subLabel:
"Capture console.log and console.error into internal buffer",
value: verbose,
onValueChange: (v: boolean) => setVerbose(v),
icon: null,
}),
),
React.createElement(
TableRowGroup,
{ title: "Actions" },
React.createElement(TableRow, {
key: "dump",
label: "Dump Logs",
subLabel: "Write recent captured logs to the debug logger",
trailing: React.createElement(TableRow.TrailingText, {
text: "Dump",
}),
onPress: () => {
if (actionsDisabled) {
showToast("Enable verbose logging first");
return;
}
dumpLogs();
},
disabled: actionsDisabled,
}),
React.createElement(TableRow, {
key: "clear",
label: "Clear Logs",
subLabel: "Clear the internal captured log buffer",
trailing: React.createElement(TableRow.TrailingText, {
text: "Clear",
}),
onPress: () => {
if (actionsDisabled) {
showToast("Enable verbose logging first");
return;
}
clearLogs();
},
disabled: actionsDisabled,
}),
React.createElement(TableRow, {
key: "emit",
label: "Emit Test Logs",
subLabel: "Emit a test console.log / console.error entry",
trailing: React.createElement(TableRow.TrailingText, {
text: "Emit",
}),
onPress: () => {
if (actionsDisabled) {
showToast("Enable verbose logging first");
return;
}
triggerTestLog();
},
disabled: actionsDisabled,
}),
),
]);
},
start() {
// Ensure settings object exists and apply patches according to stored config
settings.shiggyhelper = settings.shiggyhelper || { verboseLogging: false };
applyVerbosePatch(Boolean(settings.shiggyhelper.verboseLogging));
logger.log("ShiggyHelper: started");
},
stop() {
// Restore console if patched
applyVerbosePatch(false);
logger.log("ShiggyHelper: stopped");
},
});

View file

@ -21,9 +21,18 @@ export default function initSettings() {
useTrailing: () => `(${version})`,
},
{
key: "BUNNY_PLUGINS",
key: "CORE_PLUGINS",
title: () => Strings.PLUGINS,
icon: findAssetId("ActivitiesIcon"),
render: () => import("@core/ui/settings/pages/Plugins/CorePlugins"),
},
{
key: "BUNNY_PLUGINS",
title: () => "External Plugins",
icon:
findAssetId("DownloadIcon") ??
findAssetId("LinkIcon") ??
findAssetId("ActivitiesIcon"),
render: () => import("@core/ui/settings/pages/Plugins"),
},
{

View file

@ -0,0 +1,40 @@
import { React } from "@metro/common";
import AddonPage from "@core/ui/components/AddonPage";
import PluginCard from "@core/ui/settings/pages/Plugins/components/PluginCard";
import { corePluginInstances, registeredPlugins } from "@lib/addons/plugins";
import { UnifiedPluginModel } from "@core/ui/settings/pages/Plugins/models";
import unifyBunnyPlugin from "@core/ui/settings/pages/Plugins/models/bunny";
import { Strings } from "@core/i18n";
/**
* Core plugins settings page
*
* This page shows only the built-in core plugins. Core plugins are always
* installed (can't be uninstalled) and are registered in `corePluginInstances`.
*
* We reuse the existing `AddonPage` + `PluginCard` components and feed them
* the normalized `UnifiedPluginModel` objects produced by `unifyBunnyPlugin`.
*/
export default function CorePlugins() {
return (
<AddonPage<UnifiedPluginModel>
CardComponent={PluginCard}
title={Strings.PLUGINS}
searchKeywords={[
"name",
"description",
(p) =>
p.authors
?.map((a: any) => (typeof a === "string" ? a : a.name))
.join() || "",
]}
// Only core plugins: map corePluginInstances -> registeredPlugins -> unify
items={[...corePluginInstances.keys()]
.map((id) => registeredPlugins.get(id))
.filter((m): m is any => m !== undefined)
.map(unifyBunnyPlugin)}
ListHeaderComponent={() => null}
/>
);
}

View file

@ -510,7 +510,8 @@ export default function Plugins() {
.filter((p) => isPluginInstalled(p.id) && !isCorePlugin(p.id))
.map(unifyBunnyPlugin);
return [...corePlugins, ...vdPlugins, ...bnPlugins];
// Show only Vendetta plugins + non-core Bunny plugins (exclude core plugins)
return [...vdPlugins, ...bnPlugins];
}}
ListHeaderComponent={() => null}
installAction={{

View file

@ -29,7 +29,7 @@ export const _colorRef: InternalColorRef = {
export function updateBunnyColor(
colorManifest: ColorManifest | null,
{ update = true },
{ update = true, startup = true },
) {
if (settings.safeMode?.enabled) return;
@ -60,6 +60,15 @@ export function updateBunnyColor(
}
if (update) {
AppearanceManager.setShouldSyncAppearanceSettings(false);
AppearanceManager.updateTheme(
internalDef != null ? ref.key : ref.lastSetDiscordTheme,
);
}
if (startup) {
// scuffed but it works
AppearanceManager.setShouldSyncAppearanceSettings(true);
AppearanceManager.updateTheme(
internalDef != null ? ref.key : ref.lastSetDiscordTheme,
);

View file

@ -121,10 +121,10 @@ export function selectTheme(theme: VdThemeInfo | null, write = true) {
);
if (theme == null && write) {
updateBunnyColor(null, { update: true });
updateBunnyColor(null, { update: false, startup: true });
return writeThemeToNative({});
} else if (theme) {
updateBunnyColor(theme.data, { update: true });
updateBunnyColor(theme.data, { update: true, startup: false });
return writeThemeToNative(theme);
}
}

2
src/modules.d.ts vendored
View file

@ -1,5 +1,5 @@
declare module "bunny-build-info" {
const version: "1.1.0-3";
const version: "1.1.0";
}
declare module "*.png" {