xref: /MusicFree/src/lib/react-native-vdebug/index.js (revision 5589cdf32b2bb0f641e5ac7bf1f6152cd6b9b70e)
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