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