/// 魔改自 https://github.com/itenl/react-native-vdebug
import PropTypes from 'prop-types';
import React, {PureComponent} from 'react';
import {
Animated,
Dimensions,
Keyboard,
KeyboardAvoidingView,
NativeModules,
PanResponder,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import event from './src/event';
// import Network, { traceNetwork } from './src/network';
import Log, {traceLog} from './src/log';
import HocComp from './src/hoc';
import Storage from './src/storage';
import {replaceReg} from './src/tool';
const {width, height} = Dimensions.get('window');
let commandContext = global;
export const setExternalContext = externalContext => {
if (externalContext) commandContext = externalContext;
};
// Log/network trace when Element is not initialized.
export const initTrace = () => {
traceLog();
// traceNetwork();
};
class VDebug extends PureComponent {
static propTypes = {
// Expansion panel (Optional)
panels: PropTypes.array,
};
static defaultProps = {
panels: null,
};
constructor(props) {
super(props);
initTrace();
this.containerHeight = (height / 3) * 2;
this.refsObj = {};
this.state = {
commandValue: '',
showPanel: false,
currentPageIndex: 0,
pan: new Animated.ValueXY(),
scale: new Animated.Value(1),
panelHeight: new Animated.Value(0),
panels: this.addPanels(),
history: [],
historyFilter: [],
showHistory: false,
};
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
this.state.pan.setOffset({
x: this.state.pan.x._value,
y: this.state.pan.y._value,
});
this.state.pan.setValue({x: 0, y: 0});
Animated.spring(this.state.scale, {
useNativeDriver: true,
toValue: 1.3,
friction: 7,
}).start();
},
onPanResponderMove: Animated.event([
null,
{dx: this.state.pan.x, dy: this.state.pan.y},
]),
onPanResponderRelease: ({nativeEvent}, gestureState) => {
if (
Math.abs(gestureState.dx) < 5 &&
Math.abs(gestureState.dy) < 5
)
this.togglePanel();
setTimeout(() => {
Animated.spring(this.state.scale, {
useNativeDriver: true,
toValue: 1,
friction: 7,
}).start(() => {
this.setState({
top: nativeEvent.pageY,
});
});
this.state.pan.flattenOffset();
}, 0);
},
});
}
componentDidMount() {
this.state.pan.setValue({x: 0, y: 0});
Storage.support() &&
Storage.get('react-native-vdebug@history').then(res => {
if (res) {
this.setState({
history: res,
});
}
});
}
getRef(index) {
return ref => {
if (!this.refsObj[index]) this.refsObj[index] = ref;
};
}
addPanels() {
let defaultPanels = [
{
title: 'Log',
component: HocComp(Log, this.getRef(0)),
},
// {
// title: 'Network',
// component: HocComp(Network, this.getRef(1))
// },
];
if (this.props.panels && this.props.panels.length) {
this.props.panels.forEach((item, index) => {
// support up to five extended panels
if (index >= 3) return;
if (item.title && item.component) {
item.component = HocComp(
item.component,
this.getRef(defaultPanels.length),
);
defaultPanels.push(item);
}
});
}
return defaultPanels;
}
togglePanel() {
this.state.panelHeight.setValue(
this.state.panelHeight._value ? 0 : this.containerHeight,
);
}
clearLogs() {
const tabName = this.state.panels[this.state.currentPageIndex].title;
event.trigger('clear', tabName);
}
showDev() {
NativeModules?.DevMenu?.show();
}
reloadDev() {
NativeModules?.DevMenu?.reload();
}
evalInContext(js, context) {
return function (str) {
let result = '';
try {
// eslint-disable-next-line no-eval
result = eval(str);
} catch (err) {
result = 'Invalid input';
}
return event.trigger('addLog', result);
}.call(context, `with(this) { ${js} } `);
}
execCommand() {
if (!this.state.commandValue) return;
this.evalInContext(this.state.commandValue, commandContext);
this.syncHistory();
Keyboard.dismiss();
}
clearCommand() {
this.textInput.clear();
this.setState({
historyFilter: [],
});
}
scrollToPage(index, animated = true) {
this.scrollToCard(index, animated);
}
scrollToCard(cardIndex, animated = true) {
if (cardIndex < 0) cardIndex = 0;
else if (cardIndex >= this.cardCount) cardIndex = this.cardCount - 1;
if (this.scrollView) {
this.scrollView.scrollTo({
x: width * cardIndex,
y: 0,
animated: animated,
});
}
}
scrollToTop() {
const item = this.refsObj[this.state.currentPageIndex];
const instance = item?.getScrollRef && item?.getScrollRef();
if (instance) {
// FlatList
instance.scrollToOffset &&
instance.scrollToOffset({
animated: true,
viewPosition: 0,
index: 0,
});
// ScrollView
instance.scrollTo &&
instance.scrollTo({x: 0, y: 0, animated: true});
}
}
renderPanelHeader() {
return (
{this.state.panels.map((item, index) => (
{
if (index != this.state.currentPageIndex) {
this.scrollToPage(index);
this.setState({currentPageIndex: index});
} else {
this.scrollToTop();
}
}}
style={[
styles.panelHeaderItem,
index === this.state.currentPageIndex &&
styles.activeTab,
]}>
{item.title}
))}
);
}
syncHistory() {
if (!Storage.support()) return;
const res = this.state.history.filter(f => {
return f == this.state.commandValue;
});
if (res && res.length) return;
this.state.history.splice(0, 0, this.state.commandValue);
this.state.historyFilter.splice(0, 0, this.state.commandValue);
this.setState(
{
history: this.state.history,
historyFilter: this.state.historyFilter,
},
() => {
Storage.save('react-native-vdebug@history', this.state.history);
this.forceUpdate();
},
);
}
onChange(text) {
const state = {commandValue: text};
if (text) {
const res = this.state.history.filter(f =>
f.toLowerCase().match(replaceReg(text)),
);
if (res && res.length) state.historyFilter = res;
} else {
state.historyFilter = [];
}
this.setState(state);
}
renderCommandBar() {
return (
{this.state.historyFilter.map(text => {
return (
{
if (text && text.toString) {
this.setState({
commandValue: text.toString(),
});
}
}}>
{text}
);
})}
{
this.textInput = ref;
}}
style={styles.commandBarInput}
placeholderTextColor={'#000000a1'}
placeholder="Command..."
onChangeText={this.onChange.bind(this)}
value={this.state.commandValue}
onFocus={() => {
this.setState({showHistory: true});
}}
onSubmitEditing={this.execCommand.bind(this)}
/>
X
OK
);
}
renderPanelFooter() {
return (
Clear
{__DEV__ && Platform.OS == 'ios' && (
Dev
)}
Hide
);
}
onScrollAnimationEnd({nativeEvent}) {
const currentPageIndex = Math.floor(
nativeEvent.contentOffset.x / Math.floor(width),
);
currentPageIndex != this.state.currentPageIndex &&
this.setState({
currentPageIndex: currentPageIndex,
});
}
renderPanel() {
return (
{this.renderPanelHeader()}
{
this.scrollView = ref;
}}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
horizontal={true}
style={styles.panelContent}>
{this.state.panels.map((item, index) => {
return (
);
})}
{this.renderCommandBar()}
{this.renderPanelFooter()}
);
}
renderDebugBtn() {
const {pan, scale} = this.state;
const [translateX, translateY] = [pan.x, pan.y];
const btnStyle = {transform: [{translateX}, {translateY}, {scale}]};
return (
调试
);
}
render() {
return (
{this.renderPanel()}
{this.renderDebugBtn()}
);
}
}
const styles = StyleSheet.create({
activeTab: {
backgroundColor: '#fff',
},
panel: {
position: 'absolute',
zIndex: 99998,
elevation: 99998,
backgroundColor: '#fff',
width,
bottom: 0,
right: 0,
},
panelHeader: {
width,
backgroundColor: '#eee',
flexDirection: 'row',
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#d9d9d9',
},
panelHeaderItem: {
flex: 1,
height: 40,
color: '#000',
borderRightWidth: StyleSheet.hairlineWidth,
borderColor: '#d9d9d9',
justifyContent: 'center',
},
panelHeaderItemText: {
textAlign: 'center',
},
panelContent: {
width,
flex: 0.9,
},
panelBottom: {
width,
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#d9d9d9',
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#eee',
height: 40,
},
panelBottomBtn: {
flex: 1,
height: 40,
borderRightWidth: StyleSheet.hairlineWidth,
borderColor: '#d9d9d9',
justifyContent: 'center',
},
panelBottomBtnText: {
color: '#000',
fontSize: 14,
textAlign: 'center',
},
panelEmpty: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
homeBtn: {
width: 60,
paddingVertical: 5,
backgroundColor: '#04be02',
borderRadius: 4,
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
zIndex: 99999,
bottom: height / 2,
right: 0,
shadowColor: 'rgb(18,34,74)',
shadowOffset: {width: 0, height: 1},
shadowOpacity: 0.08,
elevation: 99999,
},
homeBtnText: {
color: '#fff',
},
commandBar: {
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#d9d9d9',
flexDirection: 'row',
height: 40,
},
commandBarInput: {
flex: 1,
paddingLeft: 10,
backgroundColor: '#ffffff',
color: '#000000',
},
commandBarBtn: {
width: 40,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#eee',
},
historyContainer: {
borderTopWidth: 1,
borderTopColor: '#d9d9d9',
backgroundColor: '#ffffff',
paddingHorizontal: 10,
},
});
export default VDebug;