New setup from scratch - all modules updated - app now in subfolder
This commit is contained in:
106
SwimTracker/views/ConnectingView.js
Normal file
106
SwimTracker/views/ConnectingView.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
TextInput,
|
||||
ActivityIndicator,
|
||||
} from "react-native";
|
||||
|
||||
import SetupView from '../components/SetupView';
|
||||
import { connect } from 'react-redux';
|
||||
import { changeSwimTrackerHostname } from '../state/Reducer';
|
||||
import { i18n } from '../utility/i18n';
|
||||
|
||||
const validHostnameRegex = /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))|(^\s*((?=.{1,255}$)(?=.*[A-Za-z].*)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*)\s*$)/;
|
||||
|
||||
function isValidHostname(hostname) {
|
||||
return validHostnameRegex.test(hostname);
|
||||
}
|
||||
|
||||
function ConnectingView(props) {
|
||||
|
||||
const [isHostnameValid, setIsHostnameValid] = useState(isValidHostname(props.swimTrackerHost));
|
||||
const [hostnameTextInput, setHostnameTextInput] = useState(props.swimTrackerHost);
|
||||
const [advancedMode, setAdvancedMode] = useState(false);
|
||||
|
||||
|
||||
let onHostnameChange = newHostName => {
|
||||
setHostnameTextInput(newHostName);
|
||||
const newHostnameValid = isValidHostname(newHostName);
|
||||
setIsHostnameValid(newHostnameValid);
|
||||
if (newHostnameValid) {
|
||||
props.dispatch(changeSwimTrackerHostname(newHostName));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const hiddenStyle = advancedMode ? {} : {"display": "none"};
|
||||
|
||||
return (
|
||||
<SetupView
|
||||
headerText={i18n.t("connecting") + "..."}
|
||||
lowerLeftButtonText={ i18n.t(advancedMode ? 'simpleMode' : 'advancedMode') }
|
||||
onLowerLeftButtonPress={() => { setAdvancedMode(!advancedMode); }}
|
||||
lowerRightButtonText={i18n.t('help')}
|
||||
>
|
||||
<View style={{flexDirection: "row", alignItems: "center"}}>
|
||||
<ActivityIndicator size="large" color="#ffffff" />
|
||||
<Text style={styles.subtext}>
|
||||
{i18n.t('connectSubtext')}
|
||||
</Text>
|
||||
</View>
|
||||
{true &&
|
||||
<View style={StyleSheet.flatten([styles.row, hiddenStyle])}>
|
||||
<Text style={styles.label}>Host:</Text>
|
||||
<TextInput
|
||||
onChangeText={onHostnameChange}
|
||||
value={hostnameTextInput}
|
||||
style={{ ...styles.hostnameInput, color: isHostnameValid ? "rgb(255, 255, 255)" : "rgb(255, 150, 150)" }}
|
||||
placeholderTextColor="rgba(255,255,255,0.5)"
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
</SetupView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
subtext: {
|
||||
color: "rgba(255,255,255,1)",
|
||||
textAlign: "left",
|
||||
fontSize: 16,
|
||||
lineHeight: 25,
|
||||
width: "100%",
|
||||
//paddingTop: 50,
|
||||
paddingLeft: 30,
|
||||
},
|
||||
row: {
|
||||
backgroundColor: "rgba(255,255,255,0.4)",
|
||||
borderRadius: 5,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: 60,
|
||||
marginBottom: 5,
|
||||
},
|
||||
hostnameInput: {
|
||||
height: 30,
|
||||
color: "rgba(255,255,255,1)",
|
||||
width: "80%",
|
||||
fontSize: 18,
|
||||
},
|
||||
label : {
|
||||
color: "rgb(80, 80, 80)",
|
||||
marginLeft: 15,
|
||||
marginRight: 10,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return { swimTrackerHost: state.settings.swimTrackerHost };
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(ConnectingView);
|
||||
278
SwimTracker/views/LastSessionsView.js
Normal file
278
SwimTracker/views/LastSessionsView.js
Normal file
@@ -0,0 +1,278 @@
|
||||
import React from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
View,
|
||||
StatusBar,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
RefreshControl,
|
||||
} from "react-native";
|
||||
import themeColors from '../components/themeColors';
|
||||
import EntypoIcon from "react-native-vector-icons/Entypo";
|
||||
import AntDesignIcon from "react-native-vector-icons/AntDesign";
|
||||
import FaIcon from "react-native-vector-icons/FontAwesome5";
|
||||
import ImageHeader from "../components/ImageHeader";
|
||||
import { SwipeListView } from 'react-native-swipe-list-view';
|
||||
import { connect } from 'react-redux';
|
||||
import request from '../utility/PromiseRequest';
|
||||
import DataAnalysis from '../data_processing/DataAnalysis';
|
||||
import * as msgpack from 'msgpack-lite';
|
||||
import { timeSince } from '../utility/TimeUtils';
|
||||
import XMLParser from 'react-xml-parser';
|
||||
import {i18n} from '../utility/i18n';
|
||||
|
||||
|
||||
function SessionCard(props) {
|
||||
return (
|
||||
<View style={sessionCardStyles.card}>
|
||||
<View>
|
||||
<Text style={sessionCardStyles.firstLineText}>{props.textFirstLine}</Text>
|
||||
</View>
|
||||
|
||||
<View style={sessionCardStyles.secondLine}>
|
||||
<View style={sessionCardStyles.iconTextPair}>
|
||||
<FaIcon name="stopwatch" style={sessionCardStyles.icon} />
|
||||
<Text style={sessionCardStyles.secondLineText}>{props.activeTime}</Text>
|
||||
</View>
|
||||
<View style={sessionCardStyles.iconTextPair}>
|
||||
<EntypoIcon name="ruler" style={sessionCardStyles.icon} />
|
||||
<Text style={sessionCardStyles.secondLineText}>{props.momentum}</Text>
|
||||
</View>
|
||||
<View style={sessionCardStyles.iconTextPair}>
|
||||
<AntDesignIcon name="retweet" style={sessionCardStyles.icon} />
|
||||
<Text style={sessionCardStyles.secondLineText}>{props.laps}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function SessionCardBehindSwipe(props) {
|
||||
return (
|
||||
<View style={sessionCardStyles.rowBack}>
|
||||
<TouchableOpacity
|
||||
style={sessionCardStyles.deleteButton}
|
||||
onPress={props.onDelete}
|
||||
>
|
||||
<Text style={{ fontSize: 18, color: "white" }}>Löschen</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const sessionCardStyles = StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: "#559ac8",
|
||||
borderRadius: 12,
|
||||
height: 100,
|
||||
maxHeight: 100,
|
||||
flex: 1,
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
padding: 10,
|
||||
margin: 10,
|
||||
paddingLeft: 20,
|
||||
},
|
||||
firstLineText: {
|
||||
color: "white",
|
||||
fontSize: 22
|
||||
},
|
||||
iconTextPair: {
|
||||
maxWidth: 100,
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
secondLine: {
|
||||
flex: 1,
|
||||
justifyContent: "space-between",
|
||||
alignContent: "center",
|
||||
flexDirection: "row",
|
||||
maxHeight: 30,
|
||||
marginTop: 14,
|
||||
},
|
||||
icon: {
|
||||
fontSize: 30,
|
||||
color: "white",
|
||||
paddingRight: 10,
|
||||
},
|
||||
secondLineText: {
|
||||
color: "white",
|
||||
fontSize: 18,
|
||||
},
|
||||
spacerHidden: {
|
||||
flex: 1,
|
||||
color: "black",
|
||||
},
|
||||
rowBack: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: themeColors['ALIZARIN'],
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
height: 100,
|
||||
padding: 10,
|
||||
margin: 10,
|
||||
paddingLeft: 20,
|
||||
borderRadius: 12,
|
||||
},
|
||||
deleteButton: {
|
||||
alignItems: 'center',
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
backgroundColor: themeColors['ALIZARIN'],
|
||||
top: 0,
|
||||
width: 150,
|
||||
right: 0,
|
||||
borderTopRightRadius: 12,
|
||||
borderBottomRightRadius: 12,
|
||||
},
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
function parsePropfind(text) {
|
||||
const parser = new XMLParser();
|
||||
const xmlDoc = parser.parseFromString(text);
|
||||
|
||||
//const parser = new DOMParser();
|
||||
//const xmlDoc = parser.parseFromString(text, "text/xml");
|
||||
|
||||
const responses = xmlDoc.getElementsByTagName("D:response");
|
||||
let result = [];
|
||||
for (let i = 0; i < responses.length; ++i) {
|
||||
const e = responses[i];
|
||||
const name = e.getElementsByTagName("D:href")[0].value;
|
||||
const size = e.getElementsByTagName("D:getcontentlength")[0].value;
|
||||
result.push({
|
||||
name: name,
|
||||
size: parseInt(size),
|
||||
startTime: parseInt(name.split(".")[0])
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const msgpackCodec = msgpack.createCodec();
|
||||
msgpackCodec.addExtUnpacker(205, function (byteArr) {
|
||||
const buffer = byteArr.buffer.slice(byteArr.byteOffset, byteArr.byteLength + byteArr.byteOffset);
|
||||
const result = new Int16Array(buffer);
|
||||
return result;
|
||||
});
|
||||
|
||||
|
||||
async function getSessionDetails(swimTrackerHost, sessionFileName) {
|
||||
const url = "http://" + swimTrackerHost + "/webdav/" + sessionFileName;
|
||||
const arrayBuffer = await request({ url: url, responseType: 'arraybuffer' });
|
||||
return msgpack.decode(new Uint8Array(arrayBuffer), { codec: msgpackCodec });
|
||||
}
|
||||
|
||||
async function getSessionsFromDevice(swimTrackerHost) {
|
||||
const data = await request({ url: "http://" + swimTrackerHost + "/webdav/", method: "PROPFIND" });
|
||||
return parsePropfind(data);
|
||||
}
|
||||
|
||||
async function getFullData(swimTrackerHost, analysisSettings) {
|
||||
const parsed = await getSessionsFromDevice(swimTrackerHost);
|
||||
for (let index = 0; index < parsed.length; index++) {
|
||||
const e = parsed[index];
|
||||
const sessionDetails = await getSessionDetails(swimTrackerHost, e.name);
|
||||
e.values = sessionDetails.values;
|
||||
const da = new DataAnalysis();
|
||||
e.analysis = da.analyze(analysisSettings, e.startTime, e.values);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
class LastSessionsView extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = { sessions: null, refreshing: false };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
getFullData(this.props.swimTrackerHost, this.props.analysisSettings).then(
|
||||
e => this.setState({ sessions: e })
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const deleteSession = async sessionFileName => {
|
||||
this.setState({ sessions: null });
|
||||
await request({ url: "http://" + this.props.swimTrackerHost + "/webdav/" + sessionFileName, method: "DELETE" });
|
||||
this.setState({ sessions: await getFullData(this.props.swimTrackerHost, this.props.analysisSettings) });
|
||||
};
|
||||
|
||||
const onRefresh = async () => {
|
||||
this.setState({ refreshing: true });
|
||||
const newSessions = await getFullData(this.props.swimTrackerHost, this.props.analysisSettings);
|
||||
this.setState({ sessions: newSessions, refreshing: false });
|
||||
};
|
||||
|
||||
let innerView;
|
||||
if (this.state.sessions) {
|
||||
innerView = <SwipeListView
|
||||
refreshControl={<RefreshControl refreshing={this.state.refreshing} onRefresh={onRefresh} /> }
|
||||
style={{ width: "100%" }}
|
||||
keyExtractor={item => item.startTime.toString()}
|
||||
disableRightSwipe={true}
|
||||
data={this.state.sessions.reverse()}
|
||||
renderItem={(data, rowMap) => (
|
||||
<SessionCard
|
||||
textFirstLine={timeSince(data.item.startTime)}
|
||||
laps={(data.item.analysis.peaks.size / this.props.peaksPerLap).toFixed(1)}
|
||||
momentum={Math.trunc(data.item.analysis.totalMomentum * this.props.kgFactor / 10 / 60)}
|
||||
activeTime={data.item.analysis.activeTime} />
|
||||
)}
|
||||
renderHiddenItem={(data, rowMap) => <SessionCardBehindSwipe onDelete={() => { deleteSession(data.item.name) }} />}
|
||||
leftOpenValue={0}
|
||||
rightOpenValue={-120}
|
||||
stopRightSwipe={-145}
|
||||
/>
|
||||
}
|
||||
else {
|
||||
innerView = (
|
||||
<View style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-around",
|
||||
}}>
|
||||
<ActivityIndicator size="large" color="#aaa"></ActivityIndicator>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="rgba(0,0,0,0.4)" translucent={true} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<ImageHeader
|
||||
text={ i18n.t('lastSessions').toUpperCase() }
|
||||
navigation={this.props.navigation}
|
||||
image={require("../assets/swimmer.jpg")}
|
||||
/>
|
||||
<View style={{ flex: 1, backgroundColor: themeColors["BELIZE HOLE"] }}>
|
||||
{innerView}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
swimTrackerHost: state.settings.swimTrackerHost,
|
||||
analysisSettings: state.settings.analysis,
|
||||
kgFactor: state.settings.analysis.kgFactor,
|
||||
peaksPerLap: state.settings.analysis.peaksPerLap,
|
||||
};
|
||||
};
|
||||
export default connect(mapStateToProps)(LastSessionsView);
|
||||
|
||||
240
SwimTracker/views/MainMenuView.js
Normal file
240
SwimTracker/views/MainMenuView.js
Normal file
@@ -0,0 +1,240 @@
|
||||
import React from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
StatusBar,
|
||||
ImageBackground,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
} from "react-native";
|
||||
import themeColors from '../components/themeColors';
|
||||
import MaterialIcon from "react-native-vector-icons/MaterialIcons";
|
||||
import MaterialCommIcon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
|
||||
import EntypoIcon from "react-native-vector-icons/Entypo";
|
||||
|
||||
import { i18n } from '../utility/i18n';
|
||||
|
||||
import { ConnState, startSession } from '../state/DeviceReduxCoupling';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
function LargeHeaderView(props) {
|
||||
return (
|
||||
<View style={largeHeaderStyles.container}>
|
||||
<Text style={largeHeaderStyles.titleText}>swimtracker</Text>
|
||||
<View style={largeHeaderStyles.separator}></View>
|
||||
<Text style={largeHeaderStyles.subText}>bauer.tech</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const largeHeaderStyles = StyleSheet.create({
|
||||
container: {
|
||||
height: 160,
|
||||
maxHeight: 160,
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
paddingBottom: 10,
|
||||
},
|
||||
titleText: {
|
||||
color: "rgba(255,255,255,1)",
|
||||
fontSize: 48,
|
||||
},
|
||||
subText: {
|
||||
color: "rgba(255,255,255, 0.8)",
|
||||
marginTop: 15,
|
||||
textAlign: "center",
|
||||
},
|
||||
separator: {
|
||||
height: 7,
|
||||
backgroundColor: "rgba(255,255,255,1)",
|
||||
opacity: 0.5,
|
||||
width: 230
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
function ButtonGrid(props) {
|
||||
return (
|
||||
<View style={buttonGridStyles.rowContainer}>
|
||||
<View style={buttonGridStyles.columnContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={props.onLastSessionsPress}
|
||||
style={[{ backgroundColor: themeColors["GREEN SEA"] }, buttonGridStyles.button]}
|
||||
activeOpacity={0.6}
|
||||
>
|
||||
<MaterialCommIcon name="swim" style={buttonGridStyles.icon}></MaterialCommIcon>
|
||||
<Text style={buttonGridStyles.buttonText}>{ i18n.t('lastSessions').toUpperCase().split(" ").join("\n") }</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={buttonGridStyles.columnContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={props.onSocialPress}
|
||||
style={[{ backgroundColor: "#444" }, buttonGridStyles.button]}
|
||||
activeOpacity={0.6}
|
||||
disabled={true}
|
||||
>
|
||||
<MaterialCommIcon name="account-group" style={buttonGridStyles.icon}></MaterialCommIcon>
|
||||
<Text style={buttonGridStyles.buttonText}>{ i18n.t('mainMenu_social').toUpperCase()}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={props.onSettingsPress}
|
||||
style={[{ backgroundColor: themeColors["MIDNIGHT BLUE"] }, buttonGridStyles.button]}
|
||||
activeOpacity={0.6}
|
||||
>
|
||||
<MaterialIcon name="settings" style={buttonGridStyles.icon}></MaterialIcon>
|
||||
<Text style={buttonGridStyles.buttonText}>{ i18n.t('settings').toUpperCase()}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const buttonGridStyles = StyleSheet.create({
|
||||
rowContainer: {
|
||||
flex: 1,
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
alignItems: "center",
|
||||
paddingTop: 20,
|
||||
paddingBottom: 50,
|
||||
},
|
||||
columnContainer: {
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-around",
|
||||
alignItems: "center",
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
margin: 20,
|
||||
padding: 20,
|
||||
width: 120,
|
||||
height: 130,
|
||||
borderRadius: 10,
|
||||
opacity: 0.95,
|
||||
justifyContent: "space-around",
|
||||
alignItems: "center",
|
||||
},
|
||||
buttonText: {
|
||||
color: "rgba(255,255,255,1)",
|
||||
textAlign: "center",
|
||||
fontSize: 16,
|
||||
},
|
||||
icon: {
|
||||
color: "rgba(255,255,255,1)",
|
||||
fontSize: 60,
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
function FullWidthButton(props) {
|
||||
let textStyle = [fullWidthButtonStyles.buttonText];
|
||||
let iconStyle = [fullWidthButtonStyles.icon];
|
||||
if (props.disabled) {
|
||||
textStyle.push(fullWidthButtonStyles.buttonTextDisabled);
|
||||
iconStyle.push(fullWidthButtonStyles.iconDisabled);
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={props.onPress}
|
||||
style={fullWidthButtonStyles.container}
|
||||
disabled={props.disabled}
|
||||
activeOpacity={0.6}>
|
||||
<View style={{ flex: 1, flexDirection: "row", justifyContent: "center" }}>
|
||||
<EntypoIcon name="air" style={iconStyle}></EntypoIcon>
|
||||
<Text style={textStyle}>{props.text}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
const fullWidthButtonStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
maxHeight: 70,
|
||||
backgroundColor: themeColors["WET ASPHALT"],
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
//paddingBottom: 10,
|
||||
justifyContent: "center",
|
||||
},
|
||||
buttonText: {
|
||||
padding: 10,
|
||||
color: "rgba(255,255,255,1)",
|
||||
fontSize: 25,
|
||||
fontWeight: "600",
|
||||
textAlign: "center",
|
||||
},
|
||||
buttonTextDisabled: {
|
||||
opacity: 0.3,
|
||||
},
|
||||
icon: {
|
||||
fontSize: 40,
|
||||
padding: 10,
|
||||
color: themeColors["GREEN SEA"],
|
||||
},
|
||||
iconDisabled: {
|
||||
color: themeColors["POMEGRANATE"],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
function MainMenuView(props) {
|
||||
const s = props.connState;
|
||||
let startButtonText = i18n.t('mainMenu_swimNow').toUpperCase();
|
||||
let startButtonDisabled = false;
|
||||
if (s === ConnState.DISCONNECTED) {
|
||||
startButtonText = "NICHT VERBUNDEN";
|
||||
startButtonDisabled = true;
|
||||
} else if (s === ConnState.CONNECTED_RUNNING || s === ConnState.CONNECTED_STARTING) {
|
||||
startButtonText = "TRAINING LÄUFT";
|
||||
} else if (s === ConnState.CONNECTED_STOPPING) {
|
||||
startButtonDisabled = true;
|
||||
}
|
||||
|
||||
const onStartButtonPress = () => {
|
||||
if (!props.connState !== ConnState.CONNECTED_RUNNING) {
|
||||
props.dispatch(startSession());
|
||||
}
|
||||
props.navigation.navigate('Training')
|
||||
};
|
||||
|
||||
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={{ flex: 1 }}>
|
||||
<LargeHeaderView />
|
||||
<ButtonGrid
|
||||
onSettingsPress={() => props.navigation.navigate('Settings')}
|
||||
onLastSessionsPress = {() => props.navigation.navigate("LastSessions")}
|
||||
/>
|
||||
<FullWidthButton
|
||||
onPress={onStartButtonPress}
|
||||
text={startButtonText}
|
||||
disabled={startButtonDisabled} />
|
||||
</View>
|
||||
</ImageBackground>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return { connState: state.deviceState.connState };
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(MainMenuView);
|
||||
252
SwimTracker/views/SettingsView.js
Normal file
252
SwimTracker/views/SettingsView.js
Normal file
@@ -0,0 +1,252 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
StatusBar,
|
||||
TextInput,
|
||||
Text,
|
||||
Switch,
|
||||
} from "react-native";
|
||||
import themeColors from '../components/themeColors';
|
||||
import ImageHeader from "../components/ImageHeader";
|
||||
import { connect } from 'react-redux';
|
||||
import { TouchableOpacity } from "react-native-gesture-handler";
|
||||
import request from '../utility/PromiseRequest';
|
||||
import { i18n } from '../utility/i18n';
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
function SettingsTextInput(props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Text style={settingsGroupStyles.label}>{props.label}</Text>
|
||||
<TextInput
|
||||
style={settingsGroupStyles.textInput}
|
||||
placeholder={props.placeholder}
|
||||
placeholderTextColor="rgba(167,167,167,1)"
|
||||
selectionColor='rgb(120,120,120)'
|
||||
value={props.value}
|
||||
></TextInput>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsSwitch(props) {
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
const toggleSwitch = () => setIsEnabled(previousState => !previousState);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Text style={settingsGroupStyles.label}>{props.label}</Text>
|
||||
<Switch
|
||||
value={isEnabled}
|
||||
//thumbColor={themeColors["WET ASPHALT"]}
|
||||
onValueChange={toggleSwitch}
|
||||
trackColor={{ false: "#767577", true: themeColors["NEPHRITIS"] }}
|
||||
//thumbColor={isEnabled ? "#f5dd4b" : "#f4f3f4"}
|
||||
ios_backgroundColor="grey"
|
||||
style={settingsGroupStyles.switch}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsButton(props) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Text style={settingsGroupStyles.label}>{props.label}</Text>
|
||||
<TouchableOpacity style={settingsGroupStyles.buttonTouchable} onPress={props.onPress}>
|
||||
<Text style={settingsGroupStyles.buttonText}>{props.buttonText}</Text>
|
||||
</TouchableOpacity>
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
function SettingsText(props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Text style={settingsGroupStyles.label}>{props.label}</Text>
|
||||
<Text style={settingsGroupStyles.text}>{props.text}</Text>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function SettingsSlider(props) {
|
||||
/*
|
||||
<Slider
|
||||
value={props.value}
|
||||
disabled={props.disabled}
|
||||
thumbTintColor={themeColors["WET ASPHALT"]}
|
||||
minimumTrackTintColor={themeColors["CLOUDS"]}
|
||||
maximumTrackTintColor={themeColors["CLOUDS"]}
|
||||
style={settingsGroupStyles.slider}
|
||||
/>
|
||||
*/
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Text style={settingsGroupStyles.label}>{props.label}</Text>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
function SettingsCombo(props) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
function SettingsGroup(props) {
|
||||
|
||||
return (
|
||||
<View style={settingsGroupStyles.container}>
|
||||
<Text style={settingsGroupStyles.title}>{props.title}</Text>
|
||||
<View style={settingsGroupStyles.subsettings}>
|
||||
{React.Children.map(props.children, (child, idx) =>
|
||||
<View style={idx == 0 ? [settingsGroupStyles.row, settingsGroupStyles.firstRow] : settingsGroupStyles.row}>
|
||||
{child}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const settingsGroupStyles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 20,
|
||||
paddingRight: 30,
|
||||
},
|
||||
title: {
|
||||
color: "white",
|
||||
fontSize: 20,
|
||||
fontWeight: "600",
|
||||
},
|
||||
subsettings: {
|
||||
padding: 10,
|
||||
paddingLeft: 30,
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderTopColor: "rgba(255, 255,255, 0.6)",
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
borderTopWidth: 0.5,
|
||||
minHeight: 40,
|
||||
},
|
||||
firstRow: {
|
||||
paddingTop: 0,
|
||||
borderTopWidth: 0,
|
||||
},
|
||||
label: {
|
||||
color: "white",
|
||||
fontSize: 17,
|
||||
},
|
||||
textInput: {
|
||||
color: "rgba(255,255,255,1)",
|
||||
marginTop: 8,
|
||||
textAlign: "right",
|
||||
},
|
||||
slider: {
|
||||
minWidth: 100,
|
||||
width: 150,
|
||||
//minHeight: 50,
|
||||
},
|
||||
switch: {
|
||||
//minHeight: 50,
|
||||
//minWidth: 80,
|
||||
},
|
||||
buttonText: {
|
||||
color: "rgba(255,255,255,1)",
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
},
|
||||
text : {
|
||||
color: "rgba(255,255,255,1)",
|
||||
width: "100%",
|
||||
textAlign: "right",
|
||||
},
|
||||
buttonTouchable: {
|
||||
backgroundColor: themeColors["CARROT"],
|
||||
width: 128,
|
||||
padding: 10,
|
||||
justifyContent: "center",
|
||||
borderRadius: 4,
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
async function queryDeviceFirmwareVersion(swimTrackerHost) {
|
||||
const result = await request({ url: "http://" + swimTrackerHost + "/api/status", responseType: "json" });
|
||||
return result["firmware"]["version"];
|
||||
}
|
||||
|
||||
async function queryNewestFirmwareVersion() {
|
||||
const QUERY_URL = "https://swimtracker-update.bauer.tech/VERSION";
|
||||
const result = await request({ url: QUERY_URL, responseType: "text" });
|
||||
console.log("newest firmware version, got", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
function SettingsView(props) {
|
||||
|
||||
const [deviceFirmwareVersion, setDeviceFirmwareVersion] = useState("");
|
||||
const [newestFirmwareVersion, setNewestFirmwareVersion] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([queryDeviceFirmwareVersion(props.settings.swimTrackerHost), queryNewestFirmwareVersion()]).then(
|
||||
(values) => {
|
||||
setDeviceFirmwareVersion(values[0]);
|
||||
setNewestFirmwareVersion(values[1]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const doFirmwareUpdate = () => {
|
||||
request({ url: "http://" + props.settings.swimTrackerHost + "/api/firmwareupdate", responseType: "text"});
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="rgba(0,0,0,0.4)" translucent={true} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<ImageHeader
|
||||
text={i18n.t('settings').toUpperCase()}
|
||||
navigation={props.navigation}
|
||||
image={require("../assets/infinity_pool2.jpg")}
|
||||
/>
|
||||
|
||||
<View style={{ flex: 1, backgroundColor: themeColors["BELIZE HOLE"] }}>
|
||||
<SettingsGroup title="swimtracker Device">
|
||||
<SettingsTextInput
|
||||
label="URL/IP"
|
||||
placeholder="swimtracker-????"
|
||||
value={props.settings.swimTrackerHost}
|
||||
/>
|
||||
<SettingsSwitch label="Start automatically" />
|
||||
<SettingsSwitch label="Stop automatically" />
|
||||
<SettingsButton label="Tare" buttonText="GO" onPress={props.device.conn.sendTareCommand} />
|
||||
<SettingsButton label="WiFi config" buttonText="Reset" onPress={props.device.conn.wifiResetToProvisioning} />
|
||||
<SettingsText label="Firmware version" text={deviceFirmwareVersion}></SettingsText>
|
||||
<SettingsText label="Newest Firmware" text={newestFirmwareVersion}></SettingsText>
|
||||
<SettingsButton label="Update Firmware" buttonText="GO" onPress={doFirmwareUpdate}></SettingsButton>
|
||||
</SettingsGroup>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return { settings: state.settings };
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(SettingsView);
|
||||
122
SwimTracker/views/TrainingView.js
Normal file
122
SwimTracker/views/TrainingView.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import React from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
StatusBar,
|
||||
Text,
|
||||
TouchableOpacity
|
||||
} from "react-native";
|
||||
import themeColors from '../components/themeColors';
|
||||
import EntypoIcon from "react-native-vector-icons/Entypo";
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { useKeepAwake } from 'expo-keep-awake';
|
||||
import { stopSession } from '../state/DeviceReduxCoupling';
|
||||
import CycleView from '../components/CycleView';
|
||||
import IconCard from '../components/IconCard';
|
||||
import Graph from '../components/Graph';
|
||||
import {toTimeStr} from '../utility/TimeUtils';
|
||||
|
||||
|
||||
function SmallHeaderView(props) {
|
||||
return (
|
||||
<View style={smallHeaderStyles.container}>
|
||||
<View style={smallHeaderStyles.row}>
|
||||
<TouchableOpacity onPress={() => props.navigation.goBack()}>
|
||||
<EntypoIcon name="chevron-left" style={smallHeaderStyles.backIcon}></EntypoIcon>
|
||||
</TouchableOpacity>
|
||||
<Text style={smallHeaderStyles.text}>{props.text}</Text>
|
||||
<TouchableOpacity onPress={props.onStopPressed}>
|
||||
<EntypoIcon name="controller-stop" style={smallHeaderStyles.stopIcon}></EntypoIcon>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View >
|
||||
)
|
||||
}
|
||||
|
||||
const smallHeaderStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
minHeight: 80,
|
||||
maxHeight: 80,
|
||||
height: 80,
|
||||
width: "100%",
|
||||
backgroundColor: themeColors["WET ASPHALT"],
|
||||
},
|
||||
row: {
|
||||
paddingTop: 30,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
backIcon: {
|
||||
color: "white",
|
||||
fontSize: 40,
|
||||
},
|
||||
stopIcon: {
|
||||
color: themeColors["ALIZARIN"],
|
||||
fontSize: 40,
|
||||
paddingRight: 10,
|
||||
paddingLeft: 10,
|
||||
},
|
||||
text: {
|
||||
color: "white",
|
||||
fontSize: 30,
|
||||
},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
function TrainingView(props) {
|
||||
useKeepAwake();
|
||||
|
||||
const analysis = props.session.analysis;
|
||||
const laps = (analysis.peaks.size / props.peaksPerLap).toFixed(1);
|
||||
const totalMomentum = Math.trunc(analysis.totalMomentum * props.kgFactor / 10 / 60);
|
||||
|
||||
const onStopPressed = () => {
|
||||
props.dispatch(stopSession());
|
||||
props.navigation.navigate('Home');
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<StatusBar hidden={true} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<SmallHeaderView text="TRAINING" navigation={props.navigation} onStopPressed={onStopPressed} />
|
||||
<View style={trainingViewStyles.container}>
|
||||
<CycleView>
|
||||
<IconCard label="BAHNEN" value={laps} iconName="retweet" iconType="AntDesign" />
|
||||
<IconCard label="ZÜGE" value={analysis.peaks.size} iconName="dashboard" iconType="AntDesign" />
|
||||
</CycleView>
|
||||
|
||||
<CycleView>
|
||||
<IconCard label="DAUER" value={toTimeStr(analysis.totalTime)} iconName="clock" iconType="FontAwesome5" />
|
||||
<IconCard label="AKTIVE DAUER" value={toTimeStr(analysis.activeTime)} iconName="stopwatch" iconType="FontAwesome5" />
|
||||
</CycleView>
|
||||
<IconCard label="KRAFT" value={totalMomentum} iconName="ruler" iconType="Entypo" />
|
||||
|
||||
<Graph></Graph>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const trainingViewStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: themeColors["BELIZE HOLE"],
|
||||
padding: 20,
|
||||
justifyContent: "space-around",
|
||||
}
|
||||
});
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
session: state.deviceState,
|
||||
peaksPerLap: state.settings.analysis.peaksPerLap,
|
||||
theme: state.settings.theme,
|
||||
kgFactor: state.settings.analysis.kgFactor,
|
||||
};
|
||||
};
|
||||
export default connect(mapStateToProps)(TrainingView);
|
||||
182
SwimTracker/views/WifiPasswordView.js
Normal file
182
SwimTracker/views/WifiPasswordView.js
Normal file
@@ -0,0 +1,182 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
TouchableOpacity,
|
||||
TextInput,
|
||||
Keyboard
|
||||
} from "react-native";
|
||||
import SetupView from '../components/SetupView';
|
||||
import EvilIcon from "react-native-vector-icons/EvilIcons";
|
||||
import MaterialIcon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import themeColors from '../components/themeColors';
|
||||
|
||||
function WifiPasswordView(props) {
|
||||
props = { ...props, ...props.route.params };
|
||||
|
||||
useEffect(() => {
|
||||
Keyboard.addListener("keyboardDidShow", _keyboardDidShow);
|
||||
Keyboard.addListener("keyboardDidHide", _keyboardDidHide);
|
||||
|
||||
// cleanup function
|
||||
return () => {
|
||||
Keyboard.removeListener("keyboardDidShow", _keyboardDidShow);
|
||||
Keyboard.removeListener("keyboardDidHide", _keyboardDidHide);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [keyboardStatus, setKeyboardStatus] = useState(undefined);
|
||||
const [password1, setPassword1] = useState("");
|
||||
const [password2, setPassword2] = useState("");
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
|
||||
const _keyboardDidShow = () => setKeyboardStatus(true);
|
||||
const _keyboardDidHide = () => setKeyboardStatus(false);
|
||||
|
||||
let iconName = "wifi-strength-" + props.strength;
|
||||
if (props.lock) {
|
||||
iconName += "-lock";
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
|
||||
if (props.confirmPwInput && password1 != password2)
|
||||
setErrorMsg("Passwords don't match");
|
||||
else if (password1.length < 8)
|
||||
setErrorMsg("Password has to be at least 8 characters long")
|
||||
else if (password1.length > 128)
|
||||
setErrorMsg("Password too long");
|
||||
else {
|
||||
props.onSubmit(props.ssid, password1);
|
||||
setErrorMsg("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SetupView
|
||||
headerText="WiFi Password"
|
||||
lowerRightButtonText="Need help?"
|
||||
backButton={true}
|
||||
navigation={props.navigation}
|
||||
>
|
||||
|
||||
{!keyboardStatus &&
|
||||
<Text style={styles.subtext}>
|
||||
{props.subText}
|
||||
</Text>
|
||||
}
|
||||
|
||||
<View style={styles.formContainer}>
|
||||
<View style={[styles.row, { backgroundColor: "rgba(155,155,155,0.8)" }]}>
|
||||
<MaterialIcon style={styles.ssidIcon} name={iconName}></MaterialIcon>
|
||||
<Text style={styles.ssidLabel} >{props.ssid}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.row}>
|
||||
<EvilIcon name="lock" style={styles.ssidIcon}></EvilIcon>
|
||||
<TextInput style={styles.passwordInput}
|
||||
onChangeText={setPassword1}
|
||||
autoCompleteType="password"
|
||||
placeholder="Password"
|
||||
placeholderTextColor="rgba(255,255,255,0.5)"
|
||||
secureTextEntry={true}
|
||||
></TextInput>
|
||||
</View>
|
||||
|
||||
{props.confirmPwInput &&
|
||||
< View style={styles.row}>
|
||||
<EvilIcon name="lock" style={styles.ssidIcon}></EvilIcon>
|
||||
<TextInput style={styles.passwordInput}
|
||||
onChangeText={setPassword2}
|
||||
autoCompleteType="password"
|
||||
placeholder="Repeat Password"
|
||||
placeholderTextColor="rgba(255,255,255,0.5)"
|
||||
secureTextEntry={true}
|
||||
></TextInput>
|
||||
</View>
|
||||
}
|
||||
|
||||
{errorMsg.length > 0 &&
|
||||
<View>
|
||||
<Text style={{ color: "red", paddingTop: 10, paddingLeft: 55 }}>{errorMsg}</Text>
|
||||
</View>
|
||||
}
|
||||
|
||||
<TouchableOpacity style={[styles.row, styles.button]} onPress={onSubmit}>
|
||||
<Text style={[styles.ssidLabel, { alignSelf: "center", textAlign: "center" }]}>{props.buttonText}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
</SetupView >
|
||||
);
|
||||
}
|
||||
|
||||
WifiPasswordView.defaultProps = {
|
||||
lock: true,
|
||||
strength: 2,
|
||||
ssid: "TheWLANName",
|
||||
confirmPwInput: false,
|
||||
buttonText: "Set Password",
|
||||
subText: "Please enter password for your home WiFi"
|
||||
}
|
||||
|
||||
|
||||
WifiPasswordView.defaultProps = {
|
||||
lock: true,
|
||||
strength: 3,
|
||||
ssid: "swimtracker-E2842S",
|
||||
subText: "Use this option only if you're home WiFi doesn't reach the SwimTracker. The SwimTracker creates its own WiFi with the password you set here.",
|
||||
confirmPwInput: true,
|
||||
buttonText: "Set Password",
|
||||
}
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
subtext: {
|
||||
color: "rgba(255,255,255,1)",
|
||||
textAlign: "left",
|
||||
fontSize: 16,
|
||||
lineHeight: 25,
|
||||
width: "80%",
|
||||
paddingBottom: 30,
|
||||
},
|
||||
formContainer: {
|
||||
},
|
||||
row: {
|
||||
backgroundColor: "rgba(255,255,255,0.4)",
|
||||
borderRadius: 5,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
},
|
||||
ssidLabel: {
|
||||
color: "rgba(255,255,255,1)",
|
||||
fontSize: 18,
|
||||
width: "100%"
|
||||
},
|
||||
button: {
|
||||
marginTop: 20,
|
||||
backgroundColor: themeColors["GREEN SEA"],
|
||||
justifyContent: "center"
|
||||
},
|
||||
ssidIcon: {
|
||||
fontSize: 25,
|
||||
color: "rgba(255,255,255,1)",
|
||||
marginLeft: 15,
|
||||
marginRight: 15,
|
||||
},
|
||||
passwordInput: {
|
||||
height: 30,
|
||||
color: "rgba(255,255,255,1)",
|
||||
width: "100%",
|
||||
fontSize: 18,
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
export default WifiPasswordView;
|
||||
194
SwimTracker/views/WifiSelectionView.js
Normal file
194
SwimTracker/views/WifiSelectionView.js
Normal file
@@ -0,0 +1,194 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
ActivityIndicator,
|
||||
} from "react-native";
|
||||
import SetupView from '../components/SetupView';
|
||||
import MaterialIcon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { connect } from 'react-redux';
|
||||
import { changeSwimTrackerHostname } from '../state/Reducer';
|
||||
|
||||
|
||||
function WifiListElement(props) {
|
||||
let iconName = "wifi-strength-" + props.strength;
|
||||
if (props.lock) {
|
||||
iconName += "-lock";
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={props.onPress}>
|
||||
<View style={wifiListElementStyles.container}>
|
||||
<MaterialIcon style={wifiListElementStyles.icon} name={iconName}></MaterialIcon>
|
||||
<Text style={wifiListElementStyles.text} >{props.text}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
const wifiListElementStyles = {
|
||||
container: {
|
||||
backgroundColor: "rgba(255,255,255,0.4)",
|
||||
borderRadius: 5,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
icon: {
|
||||
fontSize: 25,
|
||||
color: "rgba(255,255,255,1)",
|
||||
marginLeft: 15,
|
||||
marginRight: 15,
|
||||
},
|
||||
text: {
|
||||
color: "rgba(255,255,255,1)",
|
||||
fontSize: 18,
|
||||
width: "100%"
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
class WifiSelectionView extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = { wifiInfo: [] };
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
processDeviceResponse(response) {
|
||||
// sort from strong to weak
|
||||
response.sort((e1, e2) => {
|
||||
if (e1.rssi > e2.rssi)
|
||||
return -1;
|
||||
if (e1.rssi < e2.rssi)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
});
|
||||
|
||||
let ssidsAlreadyAdded = {};
|
||||
let result = [];
|
||||
for (let i = 0; i < response.length; i++) {
|
||||
if (response[i].ssid in ssidsAlreadyAdded)
|
||||
continue;
|
||||
|
||||
const locked = (response[i].sec != "open");
|
||||
let strength = 1;
|
||||
if (response[i].rssi > -30)
|
||||
strength = 4;
|
||||
else if (response[i].rssi > -67)
|
||||
strength = 3;
|
||||
else if (response[i].rssi > -70)
|
||||
strength = 2;
|
||||
|
||||
result.push({ ssid: response[i].ssid, locked: locked, strength: strength });
|
||||
ssidsAlreadyAdded[response[i].ssid] = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let component = this;
|
||||
component.mounted = true;
|
||||
this.props.device.conn.scanWifiNetworks().then(
|
||||
(result) => {
|
||||
if(component.mounted) {
|
||||
this.setState({ wifiInfo: this.processDeviceResponse(result) })
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
let inner;
|
||||
|
||||
if (this.state.wifiInfo.length > 0) {
|
||||
inner = (
|
||||
<View style={styles.listContainer}>
|
||||
<ScrollView style={{centerContent: true, paddingTop: 20}}>
|
||||
{this.state.wifiInfo.map(e => (
|
||||
<WifiListElement
|
||||
text={e.ssid}
|
||||
strength={e.strength}
|
||||
lock={e.locked}
|
||||
key={e.ssid}
|
||||
onPress={() => {
|
||||
this.props.navigation.navigate("WifiPasswordView", {
|
||||
ssid: e.ssid,
|
||||
lock: e.locked,
|
||||
strength: e.strength,
|
||||
confirmPwInput: false,
|
||||
buttonText: "OK",
|
||||
subText: "Please enter the password for your home WiFi",
|
||||
onSubmit: (ssid, pw) => {
|
||||
console.log("1");
|
||||
this.props.device.conn.wifiSetModeSTA(ssid, pw);
|
||||
console.log("2", this.props.deviceReportedHostname, changeSwimTrackerHostname, this.props);
|
||||
this.props.dispatch(changeSwimTrackerHostname(this.props.deviceReportedHostname));
|
||||
console.log("3");
|
||||
},
|
||||
});
|
||||
}}>
|
||||
</WifiListElement>)
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
else {
|
||||
inner = (
|
||||
<View style={{ alignItems: "center", justifyContent:"center", height: "100%" }}>
|
||||
<View style={{ paddingBottom: 20 }}><Text style={{ fontSize: 16, color: "#fff"}}>Scanning WiFi networks</Text></View>
|
||||
<ActivityIndicator size="large" color="#ffffff" />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SetupView
|
||||
headerText="WiFi Connection"
|
||||
lowerLeftButtonText="My WiFi wasn't found"
|
||||
onLowerLeftButtonPress={() => {
|
||||
this.props.navigation.navigate("WifiPasswordView", {
|
||||
ssid: "swimtracker-E2842S", // todo real id here
|
||||
lock: true,
|
||||
strength: 4,
|
||||
confirmPwInput: true,
|
||||
buttonText: "Set Password",
|
||||
subText: "Use this option only if you're home WiFi doesn't reach the SwimTracker. The SwimTracker creates its own WiFi with the password you set here.",
|
||||
onSubmit: (ssid, pw) => {
|
||||
this.props.device.conn.wifiSetModeAP(pw);
|
||||
},
|
||||
});
|
||||
}}
|
||||
lowerRightButtonText="Need help?"
|
||||
>
|
||||
{inner}
|
||||
</SetupView>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
listContainer: {
|
||||
height: "75%",
|
||||
flex: 1,
|
||||
}
|
||||
});
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return { deviceReportedHostname: state.deviceState.deviceReportedHostname };
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(WifiSelectionView);
|
||||
Reference in New Issue
Block a user