feat(plugins/developer-settings/react-devtools): add settings
This commit is contained in:
parent
7a1ff7ef0c
commit
ce726accca
10 changed files with 160 additions and 59 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue