New setup from scratch - all modules updated - app now in subfolder
This commit is contained in:
82
SwimTracker/components/CycleView.js
Normal file
82
SwimTracker/components/CycleView.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { Animated, TouchableWithoutFeedback, View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
export default class CycleView extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
activeView: props.initialView
|
||||
};
|
||||
|
||||
this.opacityArr = React.Children.map(props.children,
|
||||
(c, i) => { console.log("iter", c, i); return new Animated.Value(i === props.initialView ? 1 : 0); });
|
||||
|
||||
this.fadeInAnimations = this.opacityArr.map(a => Animated.timing(a, {
|
||||
toValue: 1,
|
||||
duration: props.fadeDuration,
|
||||
useNativeDriver: true
|
||||
}));
|
||||
this.fadeOutAnimations = this.opacityArr.map(a => Animated.timing(a, {
|
||||
toValue: 0,
|
||||
duration: props.fadeDuration,
|
||||
useNativeDriver: true
|
||||
}));
|
||||
|
||||
console.log("opacity arr length", this.opacityArr.length, React.Children.count(props.children));
|
||||
}
|
||||
|
||||
_onTouch = () => {
|
||||
const currentViewIdx = this.state.activeView;
|
||||
const nextViewIdx = (currentViewIdx + 1) % React.Children.count(this.props.children);
|
||||
this.fadeOutAnimations[currentViewIdx].start();
|
||||
this.fadeInAnimations[nextViewIdx].start();
|
||||
this.setState({ activeView: nextViewIdx });
|
||||
this.props.onViewChange(nextViewIdx);
|
||||
}
|
||||
|
||||
render() {
|
||||
const children = React.Children.map(this.props.children, (c, i) => c);
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={this._onTouch} styles={{ flex: 1 }}>
|
||||
<View styles={{ flex: 1, flexDirection: 'row', width: "100%" }}>
|
||||
{
|
||||
React.Children.map(this.props.children, (c, i) => {
|
||||
return <Animated.View style={{
|
||||
position: i === 0 ? 'relative' : 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
opacity: this.opacityArr[i],
|
||||
transform: [{
|
||||
scale: this.opacityArr[i].interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [this.props.minScaleOnFade, 1],
|
||||
})
|
||||
}]
|
||||
}}>
|
||||
{c}
|
||||
</Animated.View>
|
||||
})
|
||||
}
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CycleView.propTypes = {
|
||||
initialView: PropTypes.number,
|
||||
fadeDuration: PropTypes.number,
|
||||
minScaleOnFade: PropTypes.number,
|
||||
onViewChange: PropTypes.func,
|
||||
}
|
||||
|
||||
CycleView.defaultProps = {
|
||||
initialView: 0,
|
||||
fadeDuration: 600,
|
||||
minScaleOnFade: 0.4,
|
||||
onViewChange: viewIdx => null,
|
||||
}
|
||||
107
SwimTracker/components/Graph.js
Normal file
107
SwimTracker/components/Graph.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import Svg, { Polyline, Text, Circle } from 'react-native-svg';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
function computeTickMarks(largest, mostTicks) {
|
||||
const minimum = largest / mostTicks
|
||||
const magnitude = 10 ** Math.floor(Math.log10(minimum))
|
||||
const residual = minimum / magnitude
|
||||
let tickInterval = 0;
|
||||
if (residual > 5)
|
||||
tickInterval = 10 * magnitude;
|
||||
else if (residual > 2)
|
||||
tickInterval = 5 * magnitude;
|
||||
else if (residual > 1)
|
||||
tickInterval = 2 * magnitude;
|
||||
else
|
||||
tickInterval = magnitude;
|
||||
|
||||
let result = [];
|
||||
let nextTick = tickInterval;
|
||||
while (nextTick < largest) {
|
||||
result.push(nextTick);
|
||||
nextTick += tickInterval;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const Graph = props => {
|
||||
const graphHeight = 100;
|
||||
const numPoints = 300;
|
||||
const yLabelSpace = 40;
|
||||
const graphWidth = numPoints + yLabelSpace;
|
||||
const minKgScale = 4; // scale such that upper graph value is n kg
|
||||
|
||||
const data = props.data.slice(-numPoints);
|
||||
const maxValueDeviceCoord = data.reduce((running, x) => Math.max(x, running), minKgScale / props.kgFactor);
|
||||
const maxValueKg = Math.max(maxValueDeviceCoord * props.kgFactor, minKgScale);
|
||||
|
||||
const devCoordToSvgX = x => yLabelSpace + x;
|
||||
const devCoordToSvgY = y => graphHeight - (y * 100 / maxValueDeviceCoord);
|
||||
const dataInKgToSvgCoordX = devCoordToSvgX;
|
||||
const dataInKgToSvgCoordY = y => devCoordToSvgY(y / props.kgFactor);
|
||||
|
||||
const coordStr = data.map((element, i) => `${devCoordToSvgX(i)}, ${devCoordToSvgY(element)}`).join(" ");
|
||||
const ticks = computeTickMarks(maxValueKg * 0.9, 4);
|
||||
let viewBox = `0 0 ${graphWidth} ${graphHeight}`;
|
||||
|
||||
const cutOffIndex = Math.max(0, props.data.size - numPoints);
|
||||
const peaksToDisplay = props.peaks.filter(p => p > cutOffIndex)
|
||||
const peaksXCoords = peaksToDisplay.map(p => devCoordToSvgX(p - cutOffIndex));
|
||||
const peaksYCoords = peaksToDisplay.map(p => devCoordToSvgY(props.data.get(p)));
|
||||
|
||||
return (
|
||||
<View style={{ aspectRatio: graphWidth / graphHeight }}>
|
||||
<Svg height="100%" width="100%" viewBox={viewBox} preserveAspectRatio="none" >
|
||||
<Polyline
|
||||
points={coordStr}
|
||||
stroke="black"
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
{ticks.map(tick => (
|
||||
<React.Fragment key={`fragment${tick}`} >
|
||||
<Polyline
|
||||
points={`${yLabelSpace}, ${dataInKgToSvgCoordY(tick)} ${graphWidth}, ${dataInKgToSvgCoordY(tick)}`}
|
||||
stroke="black"
|
||||
key={`line${tick}`}
|
||||
strokeWidth="1"
|
||||
strokeDasharray="5,5"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
<Text x="5" y={`${dataInKgToSvgCoordY(tick)}`}
|
||||
alignmentBaseline="center"
|
||||
key={`label${tick}`}
|
||||
fontSize="9pt">{tick} kg </Text>
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
{peaksXCoords.zip(peaksYCoords).map(peak => (
|
||||
<Circle
|
||||
cx={peak[0]}
|
||||
key={`peak${peak[0]}`}
|
||||
stroke="black"
|
||||
fill="black"
|
||||
cy={peak[1]}
|
||||
r="3"
|
||||
/>
|
||||
))}
|
||||
</Svg>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
data: state.deviceState.measurements,
|
||||
kgFactor: state.settings.analysis.kgFactor,
|
||||
peaks: state.deviceState.analysis.peaks,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(Graph);
|
||||
48
SwimTracker/components/IconCard.js
Normal file
48
SwimTracker/components/IconCard.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, Text } from 'react-native';
|
||||
import EntypoIcon from "react-native-vector-icons/Entypo";
|
||||
import AntDesignIcon from "react-native-vector-icons/AntDesign";
|
||||
import Fa5Icon from "react-native-vector-icons/FontAwesome5";
|
||||
|
||||
const IconCard = props => {
|
||||
let IconClass;
|
||||
if (props.iconType === "AntDesign") {
|
||||
IconClass = AntDesignIcon;
|
||||
}
|
||||
else if (props.iconType === "FontAwesome5") {
|
||||
IconClass = Fa5Icon;
|
||||
} else if (props.iconType === "Entypo") {
|
||||
IconClass = EntypoIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
|
||||
<View style={{ paddingLeft: 20 }}>
|
||||
<Text style={{ color: 'white', fontSize: props.fontSize, textAlign: "center" }}> {props.value}</Text>
|
||||
</View>
|
||||
<View style={{ alignItems: 'center', justifyContent: 'center', paddingLeft: 20 }}>
|
||||
<IconClass style={{ color: 'white', fontSize: 40 }} name={props.iconName} />
|
||||
<Text style={{ color: 'white', marginTop: 5 }}> {props.label}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
margin: 5,
|
||||
padding: 5,
|
||||
borderRadius: 6,
|
||||
justifyContent: 'space-between',
|
||||
}
|
||||
});
|
||||
|
||||
IconCard.defaultProps = {
|
||||
fontSize: 65,
|
||||
flex: 1
|
||||
};
|
||||
|
||||
export default IconCard;
|
||||
56
SwimTracker/components/ImageHeader.js
Normal file
56
SwimTracker/components/ImageHeader.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
import {
|
||||
View,
|
||||
ImageBackground,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
} from "react-native";
|
||||
import EntypoIcon from "react-native-vector-icons/Entypo";
|
||||
|
||||
|
||||
function ImageHeader(props) {
|
||||
return (
|
||||
<View style={imageHeaderStyles.container}>
|
||||
<ImageBackground
|
||||
source={props.image}
|
||||
resizeMode="cover"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<View style={imageHeaderStyles.row}>
|
||||
<TouchableOpacity onPress={() => props.navigation.goBack()}>
|
||||
<EntypoIcon name="chevron-left" style={imageHeaderStyles.icon}></EntypoIcon>
|
||||
</TouchableOpacity>
|
||||
<Text style={imageHeaderStyles.text}>{props.text}</Text>
|
||||
</View>
|
||||
</ImageBackground>
|
||||
</View >
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const imageHeaderStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
minHeight: 185,
|
||||
maxHeight: 185,
|
||||
height: 175,
|
||||
width: "100%",
|
||||
},
|
||||
row: {
|
||||
paddingTop: 40,
|
||||
flexDirection: "row",
|
||||
},
|
||||
icon: {
|
||||
color: "white",
|
||||
fontSize: 40,
|
||||
paddingRight: 10,
|
||||
paddingLeft: 10,
|
||||
},
|
||||
text: {
|
||||
color: "white",
|
||||
fontSize: 30,
|
||||
},
|
||||
});
|
||||
|
||||
export default ImageHeader;
|
||||
39
SwimTracker/components/PropValueCard.js
Normal file
39
SwimTracker/components/PropValueCard.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import {View, StyleSheet, Text} from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
|
||||
|
||||
const PropValueCard = props => {
|
||||
|
||||
return (
|
||||
<LinearGradient
|
||||
colors={['#0075c4', '#1A8FDE']}
|
||||
start={[0, 0]}
|
||||
end={[1, 0]}
|
||||
style={styles.gradient}>
|
||||
|
||||
<Text style={{color:'white', fontSize: 16}}>
|
||||
{props.label}
|
||||
</Text>
|
||||
<Text style={{color:'white', fontSize: 55}}>
|
||||
{props.value}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
gradient : {
|
||||
flex: 1,
|
||||
padding: 15,
|
||||
alignItems: 'center',
|
||||
borderRadius: 16,
|
||||
margin:15,
|
||||
marginRight: 4,
|
||||
height:120,
|
||||
justifyContent:'center',
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default PropValueCard;
|
||||
109
SwimTracker/components/SetupView.js
Normal file
109
SwimTracker/components/SetupView.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import React from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
StatusBar,
|
||||
ImageBackground,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
} from "react-native";
|
||||
import EntypoIcon from "react-native-vector-icons/Entypo";
|
||||
|
||||
|
||||
function AdditionalOptionsBottomBar(props) {
|
||||
return (
|
||||
<View style={bottomBarStyles.container}>
|
||||
{ props.leftText ?
|
||||
<TouchableOpacity onPress={props.onLeftPress} style={bottomBarStyles.button}>
|
||||
<Text style={bottomBarStyles.text}>{props.leftText} </Text>
|
||||
</TouchableOpacity> : <View></View>
|
||||
}
|
||||
{props.rightText &&
|
||||
<TouchableOpacity onPress={props.onRightPress} style={bottomBarStyles.button}>
|
||||
<Text style={bottomBarStyles.text}>{props.rightText}</Text>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const bottomBarStyles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingBottom: 30,
|
||||
},
|
||||
text: {
|
||||
color: "rgba(255,255,255,0.5)",
|
||||
},
|
||||
button: {
|
||||
borderStyle: "dotted"
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
function SetupView(props) {
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
|
||||
<StatusBar barStyle="light-content" backgroundColor="rgba(0,0,0,0.4)" translucent={true} />
|
||||
<ImageBackground
|
||||
source={require("../assets/pool_sky_background_blurred.jpg")}
|
||||
resizeMode="cover"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<View style={setupViewStyles.container}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
{props.backButton &&
|
||||
<TouchableOpacity onPress={() => props.navigation.goBack()}>
|
||||
<EntypoIcon name="chevron-left" style={setupViewStyles.backButton}></EntypoIcon>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
<Text style={setupViewStyles.headerText}>
|
||||
{props.headerText}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{flex: 1, justifyContent: "center"}}>
|
||||
{props.children}
|
||||
</View>
|
||||
<AdditionalOptionsBottomBar leftText={props.lowerLeftButtonText}
|
||||
onLeftPress={props.onLowerLeftButtonPress}
|
||||
rightText={props.lowerRightButtonText}
|
||||
onRightPress={props.onLowerRightButtonPress}>
|
||||
</AdditionalOptionsBottomBar>
|
||||
</View>
|
||||
</ImageBackground>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const setupViewStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: "space-between",
|
||||
width: "80%",
|
||||
marginLeft: 40,
|
||||
marginTop: 60,
|
||||
},
|
||||
headerText: {
|
||||
color: "rgba(255,255,255,1)",
|
||||
fontSize: 25,
|
||||
},
|
||||
subtext: {
|
||||
color: "rgba(255,255,255,1)",
|
||||
textAlign: "left",
|
||||
fontSize: 18,
|
||||
lineHeight: 25,
|
||||
width: "80%",
|
||||
marginBottom: 50,
|
||||
},
|
||||
backButton: {
|
||||
color: "rgba(255,255,255,1)",
|
||||
fontSize: 40
|
||||
},
|
||||
});
|
||||
|
||||
export default SetupView;
|
||||
26
SwimTracker/components/themeColors.js
Normal file
26
SwimTracker/components/themeColors.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// https://flatuicolors.com/palette/defo
|
||||
|
||||
const themeColors = {
|
||||
'TURQUOISE': "rgb(26, 188, 156)",
|
||||
"EMERALD": "rgb(46, 204, 113)",
|
||||
"PETER RIVER" : "rgb(52, 152, 219)",
|
||||
"AMETHYST" : "rgb(155, 89, 182)",
|
||||
"WET ASPHALT" : "rgb(52, 73, 94)",
|
||||
"GREEN SEA" : "rgb(22, 160, 133)",
|
||||
"NEPHRITIS" : "rgb(39, 174, 96)",
|
||||
"BELIZE HOLE" : "rgb(41, 128, 185)",
|
||||
"WISTERIA" : "rgb(142, 68, 173)",
|
||||
"MIDNIGHT BLUE" : "rgb(44, 62, 80)",
|
||||
"SUN FLOWER" : "rgb(241, 196, 15)",
|
||||
"CARROT" : "rgb(230, 126, 34)",
|
||||
"ALIZARIN" : "rgb(231, 76, 60)",
|
||||
"CLOUDS" : "rgb(236, 240, 241)",
|
||||
"CONCRETE" : "rgb(149, 165, 166)",
|
||||
"ORANGE" : "rgb(243, 156, 18)",
|
||||
"PUMPKIN" : "rgb(211, 84, 0)",
|
||||
"POMEGRANATE" : "rgb(192, 57, 43)",
|
||||
"SILVER" : "rgb(189, 195, 199)",
|
||||
"ASBESTOS" : "rgb(127, 140, 141)",
|
||||
};
|
||||
|
||||
export default themeColors;
|
||||
Reference in New Issue
Block a user