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