1/// 魔改自 https://github.com/itenl/react-native-vdebug 2import PropTypes from 'prop-types'; 3import React, {PureComponent} from 'react'; 4import { 5 Animated, 6 Dimensions, 7 Keyboard, 8 KeyboardAvoidingView, 9 NativeModules, 10 PanResponder, 11 Platform, 12 ScrollView, 13 StyleSheet, 14 Text, 15 TextInput, 16 TouchableOpacity, 17 View, 18} from 'react-native'; 19import event from './src/event'; 20// import Network, { traceNetwork } from './src/network'; 21import Log, {traceLog} from './src/log'; 22import HocComp from './src/hoc'; 23import Storage from './src/storage'; 24import {replaceReg} from './src/tool'; 25 26const {width, height} = Dimensions.get('window'); 27 28let commandContext = global; 29 30export const setExternalContext = externalContext => { 31 if (externalContext) commandContext = externalContext; 32}; 33 34// Log/network trace when Element is not initialized. 35export const initTrace = () => { 36 traceLog(); 37 // traceNetwork(); 38}; 39 40class VDebug extends PureComponent { 41 static propTypes = { 42 // Expansion panel (Optional) 43 panels: PropTypes.array, 44 }; 45 46 static defaultProps = { 47 panels: null, 48 }; 49 50 constructor(props) { 51 super(props); 52 initTrace(); 53 this.containerHeight = (height / 3) * 2; 54 this.refsObj = {}; 55 this.state = { 56 commandValue: '', 57 showPanel: false, 58 currentPageIndex: 0, 59 pan: new Animated.ValueXY(), 60 scale: new Animated.Value(1), 61 panelHeight: new Animated.Value(0), 62 panels: this.addPanels(), 63 history: [], 64 historyFilter: [], 65 showHistory: false, 66 }; 67 this.panResponder = PanResponder.create({ 68 onStartShouldSetPanResponder: () => true, 69 onPanResponderGrant: () => { 70 this.state.pan.setOffset({ 71 x: this.state.pan.x._value, 72 y: this.state.pan.y._value, 73 }); 74 this.state.pan.setValue({x: 0, y: 0}); 75 Animated.spring(this.state.scale, { 76 useNativeDriver: true, 77 toValue: 1.3, 78 friction: 7, 79 }).start(); 80 }, 81 onPanResponderMove: Animated.event([ 82 null, 83 {dx: this.state.pan.x, dy: this.state.pan.y}, 84 ]), 85 onPanResponderRelease: ({nativeEvent}, gestureState) => { 86 if ( 87 Math.abs(gestureState.dx) < 5 && 88 Math.abs(gestureState.dy) < 5 89 ) 90 this.togglePanel(); 91 setTimeout(() => { 92 Animated.spring(this.state.scale, { 93 useNativeDriver: true, 94 toValue: 1, 95 friction: 7, 96 }).start(() => { 97 this.setState({ 98 top: nativeEvent.pageY, 99 }); 100 }); 101 this.state.pan.flattenOffset(); 102 }, 0); 103 }, 104 }); 105 } 106 107 componentDidMount() { 108 this.state.pan.setValue({x: 0, y: 0}); 109 Storage.support() && 110 Storage.get('react-native-vdebug@history').then(res => { 111 if (res) { 112 this.setState({ 113 history: res, 114 }); 115 } 116 }); 117 } 118 119 getRef(index) { 120 return ref => { 121 if (!this.refsObj[index]) this.refsObj[index] = ref; 122 }; 123 } 124 125 addPanels() { 126 let defaultPanels = [ 127 { 128 title: 'Log', 129 component: HocComp(Log, this.getRef(0)), 130 }, 131 // { 132 // title: 'Network', 133 // component: HocComp(Network, this.getRef(1)) 134 // }, 135 ]; 136 if (this.props.panels && this.props.panels.length) { 137 this.props.panels.forEach((item, index) => { 138 // support up to five extended panels 139 if (index >= 3) return; 140 if (item.title && item.component) { 141 item.component = HocComp( 142 item.component, 143 this.getRef(defaultPanels.length), 144 ); 145 defaultPanels.push(item); 146 } 147 }); 148 } 149 return defaultPanels; 150 } 151 152 togglePanel() { 153 this.state.panelHeight.setValue( 154 this.state.panelHeight._value ? 0 : this.containerHeight, 155 ); 156 } 157 158 clearLogs() { 159 const tabName = this.state.panels[this.state.currentPageIndex].title; 160 event.trigger('clear', tabName); 161 } 162 163 showDev() { 164 NativeModules?.DevMenu?.show(); 165 } 166 167 reloadDev() { 168 NativeModules?.DevMenu?.reload(); 169 } 170 171 evalInContext(js, context) { 172 return function (str) { 173 let result = ''; 174 try { 175 // eslint-disable-next-line no-eval 176 result = eval(str); 177 } catch (err) { 178 result = 'Invalid input'; 179 } 180 return event.trigger('addLog', result); 181 }.call(context, `with(this) { ${js} } `); 182 } 183 184 execCommand() { 185 if (!this.state.commandValue) return; 186 this.evalInContext(this.state.commandValue, commandContext); 187 this.syncHistory(); 188 Keyboard.dismiss(); 189 } 190 191 clearCommand() { 192 this.textInput.clear(); 193 this.setState({ 194 historyFilter: [], 195 }); 196 } 197 198 scrollToPage(index, animated = true) { 199 this.scrollToCard(index, animated); 200 } 201 202 scrollToCard(cardIndex, animated = true) { 203 if (cardIndex < 0) cardIndex = 0; 204 else if (cardIndex >= this.cardCount) cardIndex = this.cardCount - 1; 205 if (this.scrollView) { 206 this.scrollView.scrollTo({ 207 x: width * cardIndex, 208 y: 0, 209 animated: animated, 210 }); 211 } 212 } 213 214 scrollToTop() { 215 const item = this.refsObj[this.state.currentPageIndex]; 216 const instance = item?.getScrollRef && item?.getScrollRef(); 217 if (instance) { 218 // FlatList 219 instance.scrollToOffset && 220 instance.scrollToOffset({ 221 animated: true, 222 viewPosition: 0, 223 index: 0, 224 }); 225 // ScrollView 226 instance.scrollTo && 227 instance.scrollTo({x: 0, y: 0, animated: true}); 228 } 229 } 230 231 renderPanelHeader() { 232 return ( 233 <View style={styles.panelHeader}> 234 {this.state.panels.map((item, index) => ( 235 <TouchableOpacity 236 key={index.toString()} 237 onPress={() => { 238 if (index != this.state.currentPageIndex) { 239 this.scrollToPage(index); 240 this.setState({currentPageIndex: index}); 241 } else { 242 this.scrollToTop(); 243 } 244 }} 245 style={[ 246 styles.panelHeaderItem, 247 index === this.state.currentPageIndex && 248 styles.activeTab, 249 ]}> 250 <Text style={styles.panelHeaderItemText}> 251 {item.title} 252 </Text> 253 </TouchableOpacity> 254 ))} 255 </View> 256 ); 257 } 258 259 syncHistory() { 260 if (!Storage.support()) return; 261 const res = this.state.history.filter(f => { 262 return f == this.state.commandValue; 263 }); 264 if (res && res.length) return; 265 this.state.history.splice(0, 0, this.state.commandValue); 266 this.state.historyFilter.splice(0, 0, this.state.commandValue); 267 this.setState( 268 { 269 history: this.state.history, 270 historyFilter: this.state.historyFilter, 271 }, 272 () => { 273 Storage.save('react-native-vdebug@history', this.state.history); 274 this.forceUpdate(); 275 }, 276 ); 277 } 278 279 onChange(text) { 280 const state = {commandValue: text}; 281 if (text) { 282 const res = this.state.history.filter(f => 283 f.toLowerCase().match(replaceReg(text)), 284 ); 285 if (res && res.length) state.historyFilter = res; 286 } else { 287 state.historyFilter = []; 288 } 289 this.setState(state); 290 } 291 292 renderCommandBar() { 293 return ( 294 <KeyboardAvoidingView 295 keyboardVerticalOffset={Platform.OS == 'android' ? 0 : 300} 296 contentContainerStyle={{flex: 1}} 297 behavior={'position'} 298 style={{ 299 height: this.state.historyFilter.length ? 120 : 40, 300 borderWidth: StyleSheet.hairlineWidth, 301 borderColor: '#d9d9d9', 302 }}> 303 <View 304 style={[ 305 styles.historyContainer, 306 {height: this.state.historyFilter.length ? 80 : 0}, 307 ]}> 308 <ScrollView> 309 {this.state.historyFilter.map(text => { 310 return ( 311 <TouchableOpacity 312 style={{ 313 borderBottomWidth: 1, 314 borderBottomColor: '#eeeeeea1', 315 }} 316 onPress={() => { 317 if (text && text.toString) { 318 this.setState({ 319 commandValue: text.toString(), 320 }); 321 } 322 }}> 323 <Text style={{lineHeight: 25}}>{text}</Text> 324 </TouchableOpacity> 325 ); 326 })} 327 </ScrollView> 328 </View> 329 <View style={styles.commandBar}> 330 <TextInput 331 ref={ref => { 332 this.textInput = ref; 333 }} 334 style={styles.commandBarInput} 335 placeholderTextColor={'#000000a1'} 336 placeholder="Command..." 337 onChangeText={this.onChange.bind(this)} 338 value={this.state.commandValue} 339 onFocus={() => { 340 this.setState({showHistory: true}); 341 }} 342 onSubmitEditing={this.execCommand.bind(this)} 343 /> 344 <TouchableOpacity 345 style={styles.commandBarBtn} 346 onPress={this.clearCommand.bind(this)}> 347 <Text>X</Text> 348 </TouchableOpacity> 349 <TouchableOpacity 350 style={styles.commandBarBtn} 351 onPress={this.execCommand.bind(this)}> 352 <Text>OK</Text> 353 </TouchableOpacity> 354 </View> 355 </KeyboardAvoidingView> 356 ); 357 } 358 359 renderPanelFooter() { 360 return ( 361 <View style={styles.panelBottom}> 362 <TouchableOpacity 363 onPress={this.clearLogs.bind(this)} 364 style={styles.panelBottomBtn}> 365 <Text style={styles.panelBottomBtnText}>Clear</Text> 366 </TouchableOpacity> 367 {__DEV__ && Platform.OS == 'ios' && ( 368 <TouchableOpacity 369 onPress={this.showDev.bind(this)} 370 onLongPress={this.reloadDev.bind(this)} 371 style={styles.panelBottomBtn}> 372 <Text style={styles.panelBottomBtnText}>Dev</Text> 373 </TouchableOpacity> 374 )} 375 <TouchableOpacity 376 onPress={this.togglePanel.bind(this)} 377 style={styles.panelBottomBtn}> 378 <Text style={styles.panelBottomBtnText}>Hide</Text> 379 </TouchableOpacity> 380 </View> 381 ); 382 } 383 384 onScrollAnimationEnd({nativeEvent}) { 385 const currentPageIndex = Math.floor( 386 nativeEvent.contentOffset.x / Math.floor(width), 387 ); 388 currentPageIndex != this.state.currentPageIndex && 389 this.setState({ 390 currentPageIndex: currentPageIndex, 391 }); 392 } 393 394 renderPanel() { 395 return ( 396 <Animated.View 397 style={[styles.panel, {height: this.state.panelHeight}]}> 398 {this.renderPanelHeader()} 399 <ScrollView 400 onMomentumScrollEnd={this.onScrollAnimationEnd.bind(this)} 401 ref={ref => { 402 this.scrollView = ref; 403 }} 404 pagingEnabled={true} 405 showsHorizontalScrollIndicator={false} 406 horizontal={true} 407 style={styles.panelContent}> 408 {this.state.panels.map((item, index) => { 409 return ( 410 <View key={index} style={{width: width}}> 411 <item.component {...(item.props ?? {})} /> 412 </View> 413 ); 414 })} 415 </ScrollView> 416 {this.renderCommandBar()} 417 {this.renderPanelFooter()} 418 </Animated.View> 419 ); 420 } 421 422 renderDebugBtn() { 423 const {pan, scale} = this.state; 424 const [translateX, translateY] = [pan.x, pan.y]; 425 const btnStyle = {transform: [{translateX}, {translateY}, {scale}]}; 426 427 return ( 428 <Animated.View 429 {...this.panResponder.panHandlers} 430 style={[styles.homeBtn, btnStyle]}> 431 <Text style={styles.homeBtnText}>调试</Text> 432 </Animated.View> 433 ); 434 } 435 436 render() { 437 return ( 438 <View style={{flex: 1}}> 439 {this.renderPanel()} 440 {this.renderDebugBtn()} 441 </View> 442 ); 443 } 444} 445 446const styles = StyleSheet.create({ 447 activeTab: { 448 backgroundColor: '#fff', 449 }, 450 panel: { 451 position: 'absolute', 452 zIndex: 99998, 453 elevation: 99998, 454 backgroundColor: '#fff', 455 width, 456 bottom: 0, 457 right: 0, 458 }, 459 panelHeader: { 460 width, 461 backgroundColor: '#eee', 462 flexDirection: 'row', 463 borderWidth: StyleSheet.hairlineWidth, 464 borderColor: '#d9d9d9', 465 }, 466 panelHeaderItem: { 467 flex: 1, 468 height: 40, 469 color: '#000', 470 borderRightWidth: StyleSheet.hairlineWidth, 471 borderColor: '#d9d9d9', 472 justifyContent: 'center', 473 }, 474 panelHeaderItemText: { 475 textAlign: 'center', 476 }, 477 panelContent: { 478 width, 479 flex: 0.9, 480 }, 481 panelBottom: { 482 width, 483 borderWidth: StyleSheet.hairlineWidth, 484 borderColor: '#d9d9d9', 485 flexDirection: 'row', 486 alignItems: 'center', 487 backgroundColor: '#eee', 488 height: 40, 489 }, 490 panelBottomBtn: { 491 flex: 1, 492 height: 40, 493 borderRightWidth: StyleSheet.hairlineWidth, 494 borderColor: '#d9d9d9', 495 justifyContent: 'center', 496 }, 497 panelBottomBtnText: { 498 color: '#000', 499 fontSize: 14, 500 textAlign: 'center', 501 }, 502 panelEmpty: { 503 flex: 1, 504 alignItems: 'center', 505 justifyContent: 'center', 506 }, 507 homeBtn: { 508 width: 60, 509 paddingVertical: 5, 510 backgroundColor: '#04be02', 511 borderRadius: 4, 512 alignItems: 'center', 513 justifyContent: 'center', 514 position: 'absolute', 515 zIndex: 99999, 516 bottom: height / 2, 517 right: 0, 518 shadowColor: 'rgb(18,34,74)', 519 shadowOffset: {width: 0, height: 1}, 520 shadowOpacity: 0.08, 521 elevation: 99999, 522 }, 523 homeBtnText: { 524 color: '#fff', 525 }, 526 commandBar: { 527 borderWidth: StyleSheet.hairlineWidth, 528 borderColor: '#d9d9d9', 529 flexDirection: 'row', 530 height: 40, 531 }, 532 commandBarInput: { 533 flex: 1, 534 paddingLeft: 10, 535 backgroundColor: '#ffffff', 536 color: '#000000', 537 }, 538 commandBarBtn: { 539 width: 40, 540 alignItems: 'center', 541 justifyContent: 'center', 542 backgroundColor: '#eee', 543 }, 544 historyContainer: { 545 borderTopWidth: 1, 546 borderTopColor: '#d9d9d9', 547 backgroundColor: '#ffffff', 548 paddingHorizontal: 10, 549 }, 550}); 551 552export default VDebug; 553