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