From ca94e6122bf279660e91202f7fa8d6afdfe2bccf Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Thu, 1 May 2025 18:21:10 +0700 Subject: [PATCH] feat(lib/discord): add initial impl of `discord/ui/settings` --- lib/discord/package.json | 9 ++++ lib/discord/src/index.ts | 1 + lib/discord/src/ui/index.ts | 1 + lib/discord/src/ui/settings/_internal.ts | 11 +++++ lib/discord/src/ui/settings/api.ts | 53 ++++++++++++++++++++++++ lib/discord/src/ui/settings/common.ts | 49 ++++++++++++++++++++++ lib/discord/src/ui/settings/index.ts | 4 ++ 7 files changed, 128 insertions(+) create mode 100644 lib/discord/src/ui/index.ts create mode 100644 lib/discord/src/ui/settings/_internal.ts create mode 100644 lib/discord/src/ui/settings/api.ts create mode 100644 lib/discord/src/ui/settings/common.ts create mode 100644 lib/discord/src/ui/settings/index.ts diff --git a/lib/discord/package.json b/lib/discord/package.json index 15fe9db..eb1102e 100644 --- a/lib/discord/package.json +++ b/lib/discord/package.json @@ -15,8 +15,17 @@ "./preinit": { "default": "./src/preinit.ts" }, + "./ui": { + "default": "./src/ui/index.ts" + }, + "./ui/settings": { + "default": "./src/ui/settings/index.ts" + }, "./types": { "types": "./types/index.ts" + }, + "./_/ui/settings": { + "default": "./src/ui/settings/_internal.ts" } } } diff --git a/lib/discord/src/index.ts b/lib/discord/src/index.ts index 16a7385..d8fa25b 100644 --- a/lib/discord/src/index.ts +++ b/lib/discord/src/index.ts @@ -7,6 +7,7 @@ export * from './preinit' export * as flux from './flux' export * as native from './native' +export * as ui from './ui' // ../discord_common/js/shared/Logger.tsx export const Logger = lookupModule(byName('Logger'))! diff --git a/lib/discord/src/ui/index.ts b/lib/discord/src/ui/index.ts new file mode 100644 index 0000000..6813118 --- /dev/null +++ b/lib/discord/src/ui/index.ts @@ -0,0 +1 @@ +export * as settings from './settings' diff --git a/lib/discord/src/ui/settings/_internal.ts b/lib/discord/src/ui/settings/_internal.ts new file mode 100644 index 0000000..ba71b3a --- /dev/null +++ b/lib/discord/src/ui/settings/_internal.ts @@ -0,0 +1,11 @@ +import type { SettingsRowConfig, SettingsSection } from './api' + +export const _suiData = { + // Settings that are in the Overview screen + sections: {}, + // Settings that are not in the Overview screen (submenus, hidden, etc.) + settings: {}, +} as { + sections: Record + settings: Record +} diff --git a/lib/discord/src/ui/settings/api.ts b/lib/discord/src/ui/settings/api.ts new file mode 100644 index 0000000..2e6702f --- /dev/null +++ b/lib/discord/src/ui/settings/api.ts @@ -0,0 +1,53 @@ +import { _suiData } from './_internal' + +import type { ComponentType, ReactNode } from 'react' + +export function addSettingsSection(key: string, section: SettingsSection) { + _suiData.sections[key] = section + return () => delete _suiData.sections[key] +} + +export function addSettingsRow(key: string, row: SettingsRowConfig) { + _suiData.settings[key] = row + return () => delete _suiData.settings[key] +} + +export function addSettingsRowToSection(sectionKey: string, rowKey: string, row: SettingsRowConfig) { + if (!_suiData.sections[sectionKey]) throw new Error(`Section "${sectionKey}" does not exist`) + _suiData.sections[sectionKey].settings[rowKey] = row + return () => delete _suiData.sections[sectionKey].settings[rowKey] +} + +export interface SettingsSection { + label: string + settings: Record +} + +interface BaseSettingsRowConfig { + title: () => string + parent: string | null + unsearchable?: boolean + IconComponent?: () => ReactNode + usePredicate?: () => boolean + useTrailing?: () => ReactNode + useDescription?: () => string + useIsDisabled?: () => boolean +} + +export interface PressableSettingsRowConfig extends BaseSettingsRowConfig { + type: 'pressable' + onPress?: () => void +} + +export interface ToggleSettingsRowConfig extends BaseSettingsRowConfig { + type: 'toggle' + useValue?: () => boolean + onValueChange?: (value: boolean) => void +} + +export interface RouteSettingsRowConfig extends BaseSettingsRowConfig { + type: 'route' + screen: { route: string; getComponent(): ComponentType } +} + +export type SettingsRowConfig = PressableSettingsRowConfig | ToggleSettingsRowConfig | RouteSettingsRowConfig diff --git a/lib/discord/src/ui/settings/common.ts b/lib/discord/src/ui/settings/common.ts new file mode 100644 index 0000000..f948b5a --- /dev/null +++ b/lib/discord/src/ui/settings/common.ts @@ -0,0 +1,49 @@ +import { lookupModule } from '@revenge-mod/modules/finders' +import { byDependencies, byProps, looseDeps, moduleStateAware, relativeDep } from '@revenge-mod/modules/finders/filters' +import { ReactJsxRuntimeModuleId, ReactModuleId, ReactNativeModuleId } from '@revenge-mod/react' +import { proxify } from '@revenge-mod/utils/proxy' + +import type { ComponentType, ReactNode } from 'react' + +export let SettingListRenderer: SettingListRenderer = proxify( + () => { + const module = lookupModule( + moduleStateAware( + byProps('SettingsList'), + byDependencies( + // biome-ignore lint/suspicious/noSparseArray: don't care + looseDeps([ + ReactModuleId, + ReactNativeModuleId, + relativeDep(1), + relativeDep(2), + , + ReactJsxRuntimeModuleId, + ]), + ), + ), + { + includeUninitialized: true, + }, + ) as SettingListRenderer | undefined + + if (module) { + // This allows the Proxy instance to be garbage collected + // after the module is initialized. + SettingListRenderer = module + gc() + return module + } + }, + { + cache: true, + hint: 'object', + }, +)! + +export interface SettingListRenderer { + SettingsList: ComponentType<{ + ListHeaderComponent: ComponentType + sections: Array<{ label: string | ReactNode; settings: string[] }> + }> +} diff --git a/lib/discord/src/ui/settings/index.ts b/lib/discord/src/ui/settings/index.ts new file mode 100644 index 0000000..add6583 --- /dev/null +++ b/lib/discord/src/ui/settings/index.ts @@ -0,0 +1,4 @@ +import { _suiData } from './_internal' + +export * from './api' +export * from './common'