1import React from 'react'; 2import { Clipboard, PixelRatio, StyleSheet } from 'react-native'; 3 4import { StyledView } from '../components/Views'; 5import DevMenuBottomSheetContext, { Context } from './DevMenuBottomSheetContext'; 6import DevMenuButton from './DevMenuButton'; 7import DevMenuCloseButton from './DevMenuCloseButton'; 8import * as DevMenu from './DevMenuModule'; 9import DevMenuOnboarding from './DevMenuOnboarding'; 10import DevMenuTaskInfo from './DevMenuTaskInfo'; 11 12type Props = { 13 task: { [key: string]: any }; 14 uuid: string; 15}; 16 17type State = { 18 enableDevMenuTools: boolean; 19 devMenuItems: { [key: string]: any }; 20 isOnboardingFinished: boolean; 21 isLoaded: boolean; 22}; 23 24// These are defined in EXVersionManager.m in a dictionary, ordering needs to be 25// done here. 26const DEV_MENU_ORDER = [ 27 'dev-live-reload', 28 'dev-hmr', 29 'dev-remote-debug', 30 'dev-reload', 31 'dev-perf-monitor', 32 'dev-inspector', 33]; 34 35const MENU_ITEMS_ICON_MAPPINGS = { 36 'dev-hmr': 'run-fast', 37 'dev-remote-debug': 'remote-desktop', 38 'dev-perf-monitor': 'speedometer', 39 'dev-inspector': 'border-style', 40}; 41 42class DevMenuView extends React.PureComponent<Props, State> { 43 static contextType = DevMenuBottomSheetContext; 44 45 context!: Context; 46 47 constructor(props, context) { 48 super(props, context); 49 50 this.state = { 51 enableDevMenuTools: false, 52 devMenuItems: {}, 53 isOnboardingFinished: false, 54 isLoaded: false, 55 }; 56 } 57 58 componentDidMount() { 59 this.loadStateAsync(); 60 } 61 62 componentDidUpdate(prevProps: Props) { 63 if (this.props.uuid !== prevProps.uuid) { 64 this.loadStateAsync(); 65 } 66 } 67 68 collapse = async () => { 69 if (this.context) { 70 await this.context.collapse(); 71 } 72 }; 73 74 collapseAndCloseDevMenuAsync = async () => { 75 await this.collapse(); 76 await DevMenu.closeAsync(); 77 }; 78 79 loadStateAsync = async () => { 80 this.setState({ isLoaded: false }); 81 82 const [enableDevMenuTools, devMenuItems, isOnboardingFinished] = await Promise.all([ 83 DevMenu.doesCurrentTaskEnableDevtoolsAsync(), 84 DevMenu.getItemsToShowAsync(), 85 DevMenu.isOnboardingFinishedAsync(), 86 ]); 87 88 this.setState({ 89 enableDevMenuTools, 90 devMenuItems, 91 isOnboardingFinished, 92 isLoaded: true, 93 }); 94 }; 95 96 onAppReload = () => { 97 this.collapse(); 98 DevMenu.reloadAppAsync(); 99 }; 100 101 onCopyTaskUrl = async () => { 102 const { manifestUrl } = this.props.task; 103 104 await this.collapseAndCloseDevMenuAsync(); 105 Clipboard.setString(manifestUrl); 106 alert(`Copied "${manifestUrl}" to the clipboard!`); 107 }; 108 109 onGoToHome = () => { 110 this.collapse(); 111 DevMenu.goToHomeAsync(); 112 }; 113 114 onPressDevMenuButton = key => { 115 DevMenu.selectItemWithKeyAsync(key); 116 }; 117 118 onOnboardingFinished = () => { 119 DevMenu.setOnboardingFinishedAsync(true); 120 this.setState({ isOnboardingFinished: true }); 121 }; 122 123 maybeRenderDevMenuTools() { 124 const devMenuItems = Object.keys(this.state.devMenuItems).sort( 125 (a, b) => DEV_MENU_ORDER.indexOf(a) - DEV_MENU_ORDER.indexOf(b) 126 ); 127 128 if (this.state.enableDevMenuTools && this.state.devMenuItems) { 129 return ( 130 <> 131 <StyledView style={styles.separator} /> 132 {devMenuItems.map(key => { 133 return this.renderDevMenuItem(key, this.state.devMenuItems[key]); 134 })} 135 </> 136 ); 137 } 138 return null; 139 } 140 141 renderDevMenuItem(key, item) { 142 const { label, isEnabled, detail } = item; 143 144 return ( 145 <DevMenuButton 146 key={key} 147 buttonKey={key} 148 label={label} 149 onPress={this.onPressDevMenuButton} 150 icon={MENU_ITEMS_ICON_MAPPINGS[key]} 151 isEnabled={isEnabled} 152 detail={detail} 153 /> 154 ); 155 } 156 157 renderContent() { 158 const { task } = this.props; 159 const { isLoaded, isOnboardingFinished } = this.state; 160 161 if (!isLoaded) { 162 return null; 163 } 164 165 return ( 166 <> 167 {!isOnboardingFinished && <DevMenuOnboarding onClose={this.onOnboardingFinished} />} 168 169 <DevMenuTaskInfo task={task} /> 170 171 <StyledView style={styles.separator} /> 172 173 <DevMenuButton buttonKey="reload" label="Reload" onPress={this.onAppReload} icon="reload" /> 174 {task && task.manifestUrl && ( 175 <DevMenuButton 176 buttonKey="copy" 177 label="Copy link to clipboard" 178 onPress={this.onCopyTaskUrl} 179 icon="clipboard-text" 180 /> 181 )} 182 <DevMenuButton buttonKey="home" label="Go to Home" onPress={this.onGoToHome} icon="home" /> 183 184 {this.maybeRenderDevMenuTools()} 185 <DevMenuCloseButton 186 style={styles.closeButton} 187 onPress={this.collapseAndCloseDevMenuAsync} 188 /> 189 </> 190 ); 191 } 192 193 render() { 194 return ( 195 <StyledView style={styles.container} darkBackgroundColor="#000"> 196 {this.renderContent()} 197 </StyledView> 198 ); 199 } 200} 201 202const styles = StyleSheet.create({ 203 container: { 204 flex: 1, 205 paddingTop: 10, 206 borderTopLeftRadius: 10, 207 borderTopRightRadius: 10, 208 }, 209 scrollView: { 210 flex: 1, 211 }, 212 buttonContainer: { 213 backgroundColor: 'transparent', 214 }, 215 separator: { 216 borderTopWidth: 1 / PixelRatio.get(), 217 height: 12, 218 marginVertical: 4, 219 marginHorizontal: -1, 220 }, 221 closeButton: { 222 position: 'absolute', 223 right: 12, 224 top: 12, 225 zIndex: 3, // should be higher than zIndex of onboarding container 226 }, 227}); 228 229export default DevMenuView; 230