1import { HomeFilledIcon, iconSize, RefreshIcon } from '@expo/styleguide-native'; 2import MaterialCommunityIcons from '@expo/vector-icons/build/MaterialCommunityIcons'; 3import { Divider, Row, Spacer, useExpoTheme, View } from 'expo-dev-client-components'; 4import * as Font from 'expo-font'; 5import React, { Fragment, useContext, useEffect, useRef } from 'react'; 6import { Clipboard } from 'react-native'; 7 8import { ClipboardIcon } from './ClipboardIcon'; 9import DevMenuBottomSheetContext from './DevMenuBottomSheetContext'; 10import { DevMenuButton } from './DevMenuButton'; 11import { DevMenuCloseButton } from './DevMenuCloseButton'; 12import { DevMenuItem } from './DevMenuItem'; 13import * as DevMenu from './DevMenuModule'; 14import { DevMenuOnboarding } from './DevMenuOnboarding'; 15import { DevMenuServerInfo } from './DevMenuServerInfo'; 16import { DevMenuTaskInfo } from './DevMenuTaskInfo'; 17 18type Props = { 19 task: { [key: string]: any }; 20 uuid: string; 21}; 22 23// These are defined in EXVersionManager.m in a dictionary, ordering needs to be 24// done here. 25const DEV_MENU_ORDER = [ 26 'dev-live-reload', 27 'dev-hmr', 28 'dev-remote-debug', 29 'dev-reload', 30 'dev-perf-monitor', 31 'dev-inspector', 32]; 33 34function ThemedMaterialIcon({ 35 name, 36}: { 37 name: React.ComponentProps<typeof MaterialCommunityIcons>['name']; 38}) { 39 const theme = useExpoTheme(); 40 return <MaterialCommunityIcons name={name} size={iconSize.regular} color={theme.icon.default} />; 41} 42 43const MENU_ITEMS_ICON_MAPPINGS: { 44 [key: string]: React.ReactNode; 45} = { 46 'dev-hmr': <ThemedMaterialIcon name="run-fast" />, 47 'dev-remote-debug': <ThemedMaterialIcon name="remote-desktop" />, 48 'dev-perf-monitor': <ThemedMaterialIcon name="speedometer" />, 49 'dev-inspector': <ThemedMaterialIcon name="border-style" />, 50}; 51 52export function DevMenuView({ uuid, task }: Props) { 53 const context = useContext(DevMenuBottomSheetContext); 54 55 const [enableDevMenuTools, setEnableDevMenuTools] = React.useState(false); 56 const [devMenuItems, setDevMenuItems] = React.useState<{ [key: string]: any }>({}); 57 const [isOnboardingFinished, setIsOnboardingFinished] = React.useState(false); 58 const [isLoaded, setIsLoaded] = React.useState(false); 59 60 const theme = useExpoTheme(); 61 62 const prevUUIDRef = useRef(uuid); 63 64 useEffect(function didMount() { 65 loadStateAsync(); 66 }, []); 67 68 useEffect( 69 function loadStateWhenUUIDChanges() { 70 if (prevUUIDRef.current !== uuid) { 71 loadStateAsync(); 72 } 73 74 prevUUIDRef.current = uuid; 75 }, 76 [uuid] 77 ); 78 79 async function collapse() { 80 if (context) { 81 await context.collapse(); 82 } 83 } 84 85 async function collapseAndCloseDevMenuAsync() { 86 await collapse(); 87 await DevMenu.closeAsync(); 88 } 89 90 async function loadStateAsync() { 91 setIsLoaded(false); 92 93 const [enableDevMenuTools, devMenuItems, isOnboardingFinished] = await Promise.all([ 94 DevMenu.doesCurrentTaskEnableDevtoolsAsync(), 95 DevMenu.getItemsToShowAsync(), 96 DevMenu.isOnboardingFinishedAsync(), 97 Font.loadAsync({ 98 'Inter-Black': require('../assets/Inter/Inter-Black.otf'), 99 'Inter-BlackItalic': require('../assets/Inter/Inter-BlackItalic.otf'), 100 'Inter-Bold': require('../assets/Inter/Inter-Bold.otf'), 101 'Inter-BoldItalic': require('../assets/Inter/Inter-BoldItalic.otf'), 102 'Inter-ExtraBold': require('../assets/Inter/Inter-ExtraBold.otf'), 103 'Inter-ExtraBoldItalic': require('../assets/Inter/Inter-ExtraBoldItalic.otf'), 104 'Inter-ExtraLight': require('../assets/Inter/Inter-ExtraLight.otf'), 105 'Inter-ExtraLightItalic': require('../assets/Inter/Inter-ExtraLightItalic.otf'), 106 'Inter-Regular': require('../assets/Inter/Inter-Regular.otf'), 107 'Inter-Italic': require('../assets/Inter/Inter-Italic.otf'), 108 'Inter-Light': require('../assets/Inter/Inter-Light.otf'), 109 'Inter-LightItalic': require('../assets/Inter/Inter-LightItalic.otf'), 110 'Inter-Medium': require('../assets/Inter/Inter-Medium.otf'), 111 'Inter-MediumItalic': require('../assets/Inter/Inter-MediumItalic.otf'), 112 'Inter-SemiBold': require('../assets/Inter/Inter-SemiBold.otf'), 113 'Inter-SemiBoldItalic': require('../assets/Inter/Inter-SemiBoldItalic.otf'), 114 'Inter-Thin': require('../assets/Inter/Inter-Thin.otf'), 115 'Inter-ThinItalic': require('../assets/Inter/Inter-ThinItalic.otf'), 116 }), 117 ]); 118 119 setEnableDevMenuTools(enableDevMenuTools); 120 setDevMenuItems(devMenuItems); 121 setIsOnboardingFinished(isOnboardingFinished); 122 setIsLoaded(true); 123 } 124 125 function onAppReload() { 126 collapse(); 127 DevMenu.reloadAppAsync(); 128 } 129 130 async function onCopyTaskUrl() { 131 const { manifestUrl } = task; 132 133 await collapseAndCloseDevMenuAsync(); 134 Clipboard.setString(manifestUrl); 135 alert(`Copied "${manifestUrl}" to the clipboard!`); 136 } 137 138 function onGoToHome() { 139 collapse(); 140 DevMenu.goToHomeAsync(); 141 } 142 143 function onPressDevMenuButton(key: string) { 144 DevMenu.selectItemWithKeyAsync(key); 145 } 146 147 function onOnboardingFinished() { 148 DevMenu.setOnboardingFinishedAsync(true); 149 setIsOnboardingFinished(true); 150 } 151 152 const sortedDevMenuItems = Object.keys(devMenuItems).sort( 153 (a, b) => DEV_MENU_ORDER.indexOf(a) - DEV_MENU_ORDER.indexOf(b) 154 ); 155 156 if (!isLoaded) { 157 return null; 158 } 159 160 return ( 161 <View bg="secondary" flex="1" roundedTop="large" overflow="hidden" style={{ direction: 'ltr' }}> 162 <DevMenuTaskInfo task={task} /> 163 <Divider /> 164 <View> 165 {!isOnboardingFinished && <DevMenuOnboarding onClose={onOnboardingFinished} />} 166 <DevMenuServerInfo task={task} /> 167 <Divider /> 168 <Row align="center" padding="medium"> 169 <DevMenuButton 170 buttonKey="reload" 171 label="Reload" 172 onPress={onAppReload} 173 icon={<RefreshIcon size={iconSize.small} color={theme.icon.default} />} 174 /> 175 <Spacer.Horizontal size="medium" /> 176 {task && task.manifestUrl && ( 177 <> 178 <DevMenuButton 179 buttonKey="copy" 180 label="Copy Link" 181 onPress={onCopyTaskUrl} 182 icon={<ClipboardIcon size={iconSize.regular} color={theme.icon.default} />} 183 /> 184 <Spacer.Horizontal size="medium" /> 185 </> 186 )} 187 <DevMenuButton 188 buttonKey="home" 189 label="Go Home" 190 onPress={onGoToHome} 191 icon={<HomeFilledIcon size={iconSize.regular} color={theme.icon.default} />} 192 /> 193 </Row> 194 {enableDevMenuTools && devMenuItems && ( 195 <View padding="medium" style={{ paddingTop: 0 }}> 196 <View bg="default" rounded="large"> 197 {sortedDevMenuItems.map((key, i) => { 198 const item = devMenuItems[key]; 199 200 const { label, isEnabled } = item; 201 return ( 202 <Fragment key={key}> 203 <DevMenuItem 204 buttonKey={key} 205 label={label} 206 onPress={onPressDevMenuButton} 207 icon={MENU_ITEMS_ICON_MAPPINGS[key]} 208 isEnabled={isEnabled} 209 /> 210 {i < sortedDevMenuItems.length - 1 && <Divider />} 211 </Fragment> 212 ); 213 })} 214 </View> 215 </View> 216 )} 217 </View> 218 <DevMenuCloseButton onPress={collapseAndCloseDevMenuAsync} /> 219 </View> 220 ); 221} 222