feat(plugins/developer-settings/react-devtools): add settings

This commit is contained in:
PalmDevs 2025-05-22 02:48:54 +07:00
parent 7a1ff7ef0c
commit ce726accca
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
10 changed files with 160 additions and 59 deletions

View file

@ -6,6 +6,7 @@ export const MobileSetting = {
// SUBSETTINGS
REACT_DEVTOOLS_VERSION: 'REACT_DEVTOOLS_VERSION',
REACT_DEVTOOLS_AUTO_CONNECT: 'REACT_DEVTOOLS_AUTO_CONNECT',
REACT_DEVTOOLS_CONNECT: 'REACT_DEVTOOLS_CONNECT',
REACT_DEVTOOLS_DISCONNECT: 'REACT_DEVTOOLS_DISCONNECT',
EVALUATE_JAVASCRIPT: 'EVALUATE_JAVASCRIPT',

View file

@ -0,0 +1,22 @@
import { React } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { api } from '..'
import { MobileSetting } from '../constants'
import { DevToolsContext } from '../react-devtools'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const ReactDevToolsAutoConnectSetting: SettingsItem = {
parent: MobileSetting.REVENGE_DEVELOPER,
IconComponent: () => <TableRowAssetIcon name="LinkIcon" />,
title: () => 'Auto-connect to React DevTools',
useDescription: () => 'Automatically connect to React DevTools during startup.',
usePredicate: () => DevToolsContext.available,
useValue: () => api.storage.use(s => s.devtools?.autoConnect !== undefined)!.devtools.autoConnect,
onValueChange: v => api.storage.set({ devtools: { autoConnect: v } }),
type: 'toggle',
}
export default ReactDevToolsAutoConnectSetting

View file

@ -3,7 +3,7 @@ import { React } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting } from '../constants'
import { DevToolsContext, connectToDevTools, useIsDevToolsOpen } from '../devtools'
import { DevToolsContext, connectToDevTools, useIsDevToolsOpen } from '../react-devtools'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'

View file

@ -3,7 +3,7 @@ import { React } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting } from '../constants'
import { disconnectFromDevTools, useIsDevToolsOpen } from '../devtools'
import { disconnectFromDevTools, useIsDevToolsOpen } from '../react-devtools'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'

View file

@ -3,7 +3,7 @@ import { React } from '@revenge-mod/react'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { MobileSetting } from '../constants'
import { DevToolsContext } from '../devtools'
import { DevToolsContext } from '../react-devtools'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'

View file

@ -6,7 +6,21 @@ import { isSettingsModulesLoaded, onSettingsModulesLoaded } from '@revenge-mod/d
import type { PluginApi } from '@revenge-mod/plugins/types'
registerPlugin(
interface Storage {
devtools: {
address: string
autoConnect: boolean
}
}
const defaultStorage: Storage = {
devtools: {
address: 'localhost:8097',
autoConnect: false,
},
}
registerPlugin<Storage>(
{
id: 'revenge.settings.developer',
name: 'Developer Settings',
@ -15,10 +29,19 @@ registerPlugin(
icon: 'WrenchIcon',
},
{
storage: {
load: true,
default: defaultStorage,
},
start(api_) {
api = api_
if (isSettingsModulesLoaded()) require('./register').register()
else onSettingsModulesLoaded(() => require('./register').register())
api.storage.get().then(settings => {
if (settings.devtools.autoConnect) import('./react-devtools').then(it => it.connectToDevTools())
})
},
},
PluginFlags.Enabled,
@ -26,4 +49,4 @@ registerPlugin(
)
// Expose to EvaluateJavaScriptSetting
export let api: PluginApi
export let api: PluginApi<Storage>

View file

@ -29,7 +29,7 @@ subscriptions.add((e, err) => {
api.logger.error('React DevTools error:', actualError)
ToastActionCreators.open({
key: 'DEVTOOLS_ERROR',
key: 'REACT_DEVTOOLS_ERROR',
IconComponent: CircleXIcon,
content: actualError,
})

View file

@ -4,6 +4,7 @@ import { MobileSetting } from './constants'
import AssetBrowserSetting from './definitions/AssetBrowserSetting'
import EvaluateJavaScriptSetting from './definitions/EvaluateJavaScriptSetting'
import ReactDevToolsAutoConnectSetting from './definitions/ReactDevToolsAutoConnectSetting'
import ReactDevToolsConnectSetting from './definitions/ReactDevToolsConnectSetting'
import ReactDevToolsDisconnectSetting from './definitions/ReactDevToolsDisconnectSetting'
import ReactDevToolsVersionSetting from './definitions/ReactDevToolsVersionSetting'
@ -16,6 +17,7 @@ export function register() {
registerSettingsItems({
[MobileSetting.REVENGE_DEVELOPER]: RevengeDeveloperSetting,
[MobileSetting.REACT_DEVTOOLS_VERSION]: ReactDevToolsVersionSetting,
[MobileSetting.REACT_DEVTOOLS_AUTO_CONNECT]: ReactDevToolsAutoConnectSetting,
[MobileSetting.REACT_DEVTOOLS_CONNECT]: ReactDevToolsConnectSetting,
[MobileSetting.REACT_DEVTOOLS_DISCONNECT]: ReactDevToolsDisconnectSetting,
[MobileSetting.EVALUATE_JAVASCRIPT]: EvaluateJavaScriptSetting,

View file

@ -8,12 +8,13 @@ import Page from '~/components/Page'
import SearchInput from '~/components/SearchInput'
import TableRowAssetIcon from '~/components/TableRowAssetIcon'
import { getAllAssets, getAssetId } from '@revenge-mod/assets'
import { getAllAssets, getAssetId, getAssetModuleId } from '@revenge-mod/assets'
import type { Asset, AssetId } from '@revenge-mod/assets/types'
import type { Metro } from '@revenge-mod/modules/types'
const { Image, StyleSheet, View } = ReactNative
const { AlertModal, AlertActionButton, TableRow, Text, Stack } = Design
const { Image, StyleSheet } = ReactNative
const { AlertModal, AlertActionButton, TableRow, TableRowGroup, Text } = Design
const Displayable = new Set(['png', 'jpg', 'svg', 'webp'])
const UndisplayableFallback = {
@ -27,7 +28,7 @@ const UndisplayableFallback = {
export default function AssetBrowserSettingScreen() {
const [search, setSearch] = React.useState('')
const assets = React.useMemo(
() => [...getAllAssets()].map(asset => [getAssetId(asset)!, asset] as const),
() => [...getAllAssets()].map(asset => [getAssetId(asset)!, getAssetModuleId(asset)!, asset] as const),
[getAllAssets],
)
@ -35,39 +36,58 @@ export default function AssetBrowserSettingScreen() {
() =>
!search.length
? assets
: assets.filter(([_, asset]) => asset.name.toLowerCase().includes(search.toLowerCase())),
: assets.filter(([, , asset]) => asset.name.toLowerCase().includes(search.toLowerCase())),
[assets, search],
)
return (
<Page spacing={16}>
<SearchInput size="md" onChange={(v: string) => setSearch(v)} />
<View style={styles.listContainer}>
<FlashList.FlashList
data={filteredAssets}
renderItem={({ item: [id, asset] }) => <AssetDisplay id={id} asset={asset} />}
estimatedItemSize={80}
/>
</View>
<FlashList.FlashList
data={filteredAssets}
keyExtractor={([id]) => id.toString()}
renderItem={({ item: [id, moduleId, asset], index }) => (
<AssetDisplay
start={!index}
end={index === assets.length - 1}
id={id}
moduleId={moduleId}
asset={asset}
/>
)}
estimatedItemSize={80}
/>
</Page>
)
}
function AssetDisplay({
id,
moduleId,
asset,
start,
end,
}: {
id: AssetId
moduleId: Metro.ModuleID
asset: Asset
start?: boolean
end?: boolean
}) {
const isDisplayable = Displayable.has(asset.type)
const metadata = [`ID: ${id}`, `Type: ${asset.type}`]
const metadata = [
['ID', id.toString()],
['Type', asset.type],
['Module ID', moduleId.toString()],
] as const
return (
<TableRow
start={start}
end={end}
variant={isDisplayable ? 'default' : 'danger'}
label={asset.name}
subLabel={metadata.join(' • ')}
subLabel={metadata.map(([name, value]) => `${name}: ${value}`).join(' • ')}
icon={
isDisplayable ? (
<Image source={id} style={styles.smallPreview} />
@ -82,40 +102,50 @@ function AssetDisplay({
/>
)
}
onPress={() =>
AlertActionCreators.openAlert(
'asset-display',
<AlertModal
title={asset.name}
content={metadata.join(' • ')}
extraContent={
isDisplayable ? (
<Image source={id} style={styles.preview} />
) : (
<PreviewUnavailable type={asset.type} />
)
}
actions={
<Stack>
<AlertActionButton
text="Copy asset name"
variant="primary"
onPress={() => Clipboard.setString(asset.name)}
/>
<AlertActionButton
text="Copy asset ID"
variant="secondary"
onPress={() => Clipboard.setString(id.toString())}
/>
</Stack>
}
/>,
)
}
onPress={() => openAssetDisplayAlert(asset, id, metadata)}
/>
)
}
function openAssetDisplayAlert(
asset: Asset,
id: AssetId,
metadata: Readonly<Readonly<[string, string]>[]> | [string, string][],
) {
const isDisplayable = Displayable.has(asset.type)
AlertActionCreators.openAlert(
'asset-display',
<AlertModal
title={asset.name}
extraContent={
<>
{isDisplayable ? (
<Image source={id} style={styles.preview} />
) : (
<PreviewUnavailable type={asset.type} />
)}
<TableRowGroup>
{metadata.map(([name, value]) => (
<TableRow
key={id}
label={name}
subLabel={value}
onPress={() => Clipboard.setString(value)}
/>
))}
</TableRowGroup>
<Text variant="text-xs/semibold" color="text-danger">
Note: Asset IDs and module IDs are not consistent between app launches and app versions
respectively and should only be used when absolutely needed.
</Text>
</>
}
actions={<AlertActionButton text="Close" variant="secondary" />}
/>,
)
}
function PreviewUnavailable({ type }: { type: string }) {
return (
<Text variant="text-sm/medium" color="text-danger" style={styles.centeredText}>
@ -125,11 +155,6 @@ function PreviewUnavailable({ type }: { type: string }) {
}
const styles = StyleSheet.create({
listContainer: {
flex: 1,
borderRadius: 16,
overflow: 'hidden',
},
smallPreview: {
height: 32,
width: 32,

View file

@ -3,7 +3,16 @@ import { SettingListRenderer } from '@revenge-mod/discord/modules/settings/rende
import { React, ReactNative } from '@revenge-mod/react'
import { MobileSetting } from '../constants'
import { DevToolsContext } from '../devtools'
import { DevToolsContext, useIsDevToolsOpen } from '../react-devtools'
import { api } from '..'
import { ToastActionCreators } from '@revenge-mod/discord/actions'
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
const CircleCheckIcon = lookupGeneratedIconComponent(
'CircleCheckIcon',
'CircleCheckIcon-secondary',
'CircleCheckIcon-primary',
)
// TODO(plugins/settings): debug bridge
export default function RevengeDeveloperSettingScreen() {
@ -18,6 +27,7 @@ export default function RevengeDeveloperSettingScreen() {
{
settings: [
MobileSetting.REACT_DEVTOOLS_VERSION,
MobileSetting.REACT_DEVTOOLS_AUTO_CONNECT,
MobileSetting.REACT_DEVTOOLS_CONNECT,
MobileSetting.REACT_DEVTOOLS_DISCONNECT,
],
@ -33,15 +43,33 @@ export default function RevengeDeveloperSettingScreen() {
}
export function ReactDevToolsAddressSetting() {
const open = useIsDevToolsOpen()
const settings = api.storage.use(s => s.devtools?.address)
return (
<ReactNative.View style={{ paddingHorizontal: 16, paddingTop: 16 }}>
<Design.TextInput
editable={!DevToolsContext.open}
isDisabled={DevToolsContext.open}
editable={!open}
isDisabled={open}
leadingText="ws://"
defaultValue={DevToolsContext.address}
defaultValue={settings?.devtools.address ?? DevToolsContext.address}
label="React DevTools"
onChange={text => (DevToolsContext.address = text)}
onBlur={() => {
api.storage
.set({
devtools: {
address: DevToolsContext.address,
},
})
.then(() => {
ToastActionCreators.open({
IconComponent: CircleCheckIcon,
key: 'REACT_DEVTOOLS_ADDRESS_SAVED',
content: 'Address saved',
})
})
}}
returnKeyType="done"
/>
</ReactNative.View>