RosePlugs: insert a new top-level settings section

This commit is contained in:
Ally Founder 2026-08-11 17:48:16 -04:00
parent c3cccdde0f
commit 0e9009843b
2 changed files with 199 additions and 2 deletions

View file

@ -1,7 +1,36 @@
import { logger } from "@vendetta";
import { getAssetIDByName } from "@vendetta/ui/assets";
import Settings from "./ui/Settings";
import patchRosiesPlugsSection, { SectionRow } from "./patches/settings";
let unpatch: (() => void) | undefined;
function StubPluginsScreen() {
return null;
}
function buildRows(): SectionRow[] {
return [
{
key: "ROSES_PLUGS_STUB",
title: () => "Plugins",
icon: getAssetIDByName("SettingsIcon"),
page: StubPluginsScreen,
},
];
}
export default {
onLoad: () => {},
onUnload: () => {},
onLoad: () => {
try {
unpatch = patchRosiesPlugsSection(buildRows);
} catch (e: any) {
logger.error("[RosePlugs] Failed to patch settings:", e?.message ?? e);
}
},
onUnload: () => {
unpatch?.();
unpatch = undefined;
},
settings: Settings,
};

View file

@ -0,0 +1,168 @@
import { React, NavigationNative } from "@vendetta/metro/common";
import { findByProps, findByName } from "@vendetta/metro";
import { after } from "@vendetta/patcher";
import { Forms } from "@vendetta/ui/components";
import { findInReactTree } from "@vendetta/utils";
const { FormSection, FormRow } = Forms;
const SECTION_LABEL = "ROSES_PLUGS";
const SECTION_TITLE = "Rosie's Plugs";
const tabsNavigationRef = findByProps("getRootNavigationRef");
const settingConstants = findByProps("SETTING_RENDERER_CONFIG");
const createListModule = findByProps("createList");
const SettingsOverviewScreen = findByName("SettingsOverviewScreen", false);
const TableRowIconModule = findByProps("TableRowIcon");
export interface SectionRow {
key: string;
title: () => string;
icon?: any;
page: React.ComponentType<any>;
}
function navigateToRow(row: SectionRow) {
const navigation = tabsNavigationRef.getRootNavigationRef();
const Component = row.page;
navigation.navigate("VendettaCustomPage", {
title: row.title(),
render: () => React.createElement(Component),
});
}
function buildRendererRow(row: SectionRow) {
return {
type: "pressable",
useTitle: row.title,
title: row.title,
icon: row.icon,
IconComponent:
row.icon && TableRowIconModule &&
(() => React.createElement(TableRowIconModule.TableRowIcon, { source: row.icon })),
onPress: () => navigateToRow(row),
withArrow: true,
};
}
function Section({ tabs }: { tabs: SectionRow }) {
const navigation = NavigationNative.useNavigation();
return React.createElement(FormRow, {
label: tabs.title(),
leading: tabs.icon ? React.createElement(FormRow.Icon, { source: tabs.icon }) : undefined,
trailing: React.createElement(FormRow.Arrow),
onPress: () => {
const Component = tabs.page;
navigation.navigate("VendettaCustomPage", {
title: tabs.title(),
render: () => React.createElement(Component),
});
},
});
}
function patchPanelUI(getRows: () => SectionRow[], patches: (() => void)[]) {
const target = findByProps("renderTitle", "sections");
if (!target) return;
try {
patches.push(
after("default", target, (_: any, ret: any) => {
const UserSettingsOverview = findInReactTree(
ret.props.children,
(n: any) => n.type?.name === "UserSettingsOverview"
);
if (UserSettingsOverview) {
patches.push(
after("render", UserSettingsOverview.type.prototype, (_args: any, res: any) => {
const sections = findInReactTree(
res.props.children,
(n: any) => n?.children?.[1]?.type === FormSection
)?.children;
if (sections) {
const rows = getRows();
rows.forEach((row, i) => {
sections.splice(
i,
0,
React.createElement(Section, { key: row.key, tabs: row })
);
});
}
}, true)
);
}
}, true)
);
} catch {
// This surface (the tablet/desktop-style settings panel) may not exist on this build at
// all - patchTabsUI below covers the mobile settings list, which is what actually matters.
}
}
function patchTabsUI(getRows: () => SectionRow[], patches: (() => void)[]) {
if (!settingConstants || !tabsNavigationRef) {
console.warn("[RosePlugs] Missing constants for tabs UI patch");
return;
}
let rendererConfigValue = settingConstants.SETTING_RENDERER_CONFIG;
Object.defineProperty(settingConstants, "SETTING_RENDERER_CONFIG", {
enumerable: true,
configurable: true,
get: () => {
const extra: Record<string, any> = {};
for (const row of getRows()) extra[row.key] = buildRendererRow(row);
return { ...rendererConfigValue, ...extra };
},
set(v: any) { rendererConfigValue = v; },
});
const firstRender = Symbol("roseplugs-first-render");
try {
if (!createListModule) return;
patches.push(
after("createList", createListModule, function (args: any) {
if (args[0][firstRender]) return;
args[0][firstRender] = true;
const [config] = args;
const sections = config.sections;
const rows = getRows();
if (!rows.length || !sections) return;
sections.unshift({ label: SECTION_LABEL, title: SECTION_TITLE, settings: rows.map((r) => r.key) });
})
);
} catch {
if (!SettingsOverviewScreen) return;
patches.push(
after("default", SettingsOverviewScreen, (args: any, ret: any) => {
if (args[0][firstRender]) return;
args[0][firstRender] = true;
const { sections } = findInReactTree(ret, (i: any) => i.props?.sections)?.props ?? {};
const rows = getRows();
if (!rows.length || !sections) return;
sections.unshift({ label: SECTION_LABEL, title: SECTION_TITLE, settings: rows.map((r) => r.key) });
})
);
}
}
export default function patchRosiesPlugsSection(getRows: () => SectionRow[]): () => void {
const patches: (() => void)[] = [];
patchPanelUI(getRows, patches);
patchTabsUI(getRows, patches);
return () => {
for (const p of patches) p?.();
};
}