New setup from scratch - all modules updated - app now in subfolder

This commit is contained in:
Martin Bauer
2023-09-29 22:04:52 +02:00
parent e28ab91935
commit 9b6bb7f126
63 changed files with 9633 additions and 44259 deletions

View 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);

View 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);

View 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);

View 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);

View 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);

View 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;

View 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);