Lots of changes

- internationalization
- new logo
- connection screen improved
- always try provisioning IP for connection
This commit is contained in:
Martin Bauer 2021-07-28 15:08:25 +02:00
parent 03812fe514
commit 4614a28ead
21 changed files with 8916 additions and 70 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ npm-debug.*
*.orig.* *.orig.*
web-build/ web-build/
web-report/ web-report/
/dist

27
App.js
View File

@ -13,6 +13,12 @@ import { persistStore, persistReducer } from 'redux-persist'
import hardSet from 'redux-persist/lib/stateReconciler/hardSet' import hardSet from 'redux-persist/lib/stateReconciler/hardSet'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
// Internationalization
import * as Localization from 'expo-localization';
import i18n from 'i18n-js';
import en from "./locales/en/translations";
import de from "./locales/de/translations";
// Navigation // Navigation
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack'; import { createStackNavigator } from '@react-navigation/stack';
@ -27,13 +33,23 @@ import WifiSelectionView from './views/WifiSelectionView';
import WifiPasswordView from './views/WifiPasswordView'; import WifiPasswordView from './views/WifiPasswordView';
// Set the key-value pairs for the different languages you want to support.
i18n.translations = {
en: en,
de: de,
};
i18n.locale = "de-DE"; //Localization.locale; // Set the locale once at the beginning of your app.
i18n.fallbacks = true; // When a value is missing from a language it'll fallback to another language with the key present.
console.log("locale", i18n.locale);
const persistConfig = { const persistConfig = {
key: 'root', key: 'root',
storage: AsyncStorage, storage: AsyncStorage,
stateReconciler: hardSet, // stateReconciler: hardSet,
}; };
const persistedReducer = persistReducer(persistConfig, swimtrackerReducer) const persistedReducer = persistReducer(persistConfig, swimtrackerReducer);
const store = createStore(persistedReducer); const store = createStore(persistedReducer);
const persistor = persistStore(store); const persistor = persistStore(store);
const Stack = createStackNavigator(); const Stack = createStackNavigator();
@ -112,9 +128,9 @@ export default class App extends React.Component {
/> />
<Stack.Screen <Stack.Screen
name="Settings" name="Settings"
component={SettingsView} options={screenOptions}>
options={screenOptions} {props => <SettingsView {...props} device={this.device} />}
/> </Stack.Screen>
<Stack.Screen <Stack.Screen
name="Training" name="Training"
component={TrainingView} component={TrainingView}
@ -129,7 +145,6 @@ export default class App extends React.Component {
); );
let activeView; let activeView;
if (this.state.disconnected) if (this.state.disconnected)
activeView = disconnectedView; activeView = disconnectedView;
else if (this.state.isProvisioning) else if (this.state.isProvisioning)

View File

@ -1,7 +1,7 @@
{ {
"expo": { "expo": {
"name": "swimtrainer-app", "name": "SwimTracker",
"slug": "swimtrainer-app", "slug": "swimtracker",
"privacy": "public", "privacy": "public",
"platforms": [ "platforms": [
"ios", "ios",
@ -13,7 +13,7 @@
"icon": "./assets/icon.png", "icon": "./assets/icon.png",
"splash": { "splash": {
"image": "./assets/splash.png", "image": "./assets/splash.png",
"resizeMode": "contain", "resizeMode": "cover",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"updates": { "updates": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/icon.xcf Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 814 KiB

View File

@ -32,13 +32,13 @@ function ImageHeader(props) {
const imageHeaderStyles = StyleSheet.create({ const imageHeaderStyles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
minHeight: 175, minHeight: 185,
maxHeight: 175, maxHeight: 185,
height: 175, height: 175,
width: "100%", width: "100%",
}, },
row: { row: {
paddingTop: 30, paddingTop: 40,
flexDirection: "row", flexDirection: "row",
}, },
icon: { icon: {

View File

@ -28,6 +28,7 @@ const OpCodes = {
}; };
const HEARTBEAT_TIMEOUT = 3000; const HEARTBEAT_TIMEOUT = 3000;
const PROVISIONING_IP = "192.168.42.1";
export default class SwimTrackerWebsocketConnection { export default class SwimTrackerWebsocketConnection {
constructor(swimTrackerHost, onData, onStarted, onStopped, onWifiStateInfo, onConnect, onDisconnect) { constructor(swimTrackerHost, onData, onStarted, onStopped, onWifiStateInfo, onConnect, onDisconnect) {
@ -40,11 +41,13 @@ export default class SwimTrackerWebsocketConnection {
this.onConnect = onConnect; this.onConnect = onConnect;
this.onDisconnect = onDisconnect; this.onDisconnect = onDisconnect;
const wsOptions = {
maxReconnectionDelay: 4000 // try configured URL and provisioning URL
}; const urls = [`ws://${swimTrackerHost}:81`, `ws://${PROVISIONING_IP}:81`];
let urlIndex = 0;
const urlProvider = () => urls[urlIndex++ % urls.length]; // round robin url provider
this.ws = new ReconnectingWebSocket(`ws://${swimTrackerHost}:81`, [], wsOptions); this.ws = new ReconnectingWebSocket(urlProvider, [], { maxReconnectionDelay: 3000 });
this.ws.onmessage = this._onMessage; this.ws.onmessage = this._onMessage;
this.ws.onopen = this.onConnect; this.ws.onopen = this.onConnect;
this.ws.onclose = this.onDisconnect; this.ws.onclose = this.onDisconnect;
@ -67,7 +70,8 @@ export default class SwimTrackerWebsocketConnection {
let connection = this; let connection = this;
this.pingTimeout = setTimeout(() => { this.pingTimeout = setTimeout(() => {
connection.ws.reconnect(); if(connection.ws !== null)
connection.ws.reconnect();
}, HEARTBEAT_TIMEOUT); }, HEARTBEAT_TIMEOUT);
} }
@ -90,7 +94,7 @@ export default class SwimTrackerWebsocketConnection {
this._sendMsg(OpCodes.STOP_SESSION); this._sendMsg(OpCodes.STOP_SESSION);
} }
sendTareCommand() { sendTareCommand = () => {
this._sendMsg(OpCodes.TARE); this._sendMsg(OpCodes.TARE);
} }
@ -105,7 +109,7 @@ export default class SwimTrackerWebsocketConnection {
} }
wifiResetToProvisioning() { sendTareCommand = () => {
this._sendMsg(OpCodes.WIFI_STATE_SET, { this._sendMsg(OpCodes.WIFI_STATE_SET, {
"reset_to_provisioning": true, "reset_to_provisioning": true,
}); });

View File

@ -0,0 +1,10 @@
# Based on https://docs.expo.io/distribution/turtle-cli/
npm -g install turtle-cli --legacy-peer-deps
# Needs jdk 8
apt install openjdk-8-jdk-headless
export PATH=/usr/lib/jvm/java-8-openjdk-amd64/bin/:$PATH
turtle setup:android

View File

@ -0,0 +1,11 @@
export default {
connecting: "Verbindung aufbauen",
connectSubtext: "Gehe entweder in das WLAN deines SwimTrackers oder in dein eigenes WLAN, falls du den SwimTracker schon eingerichtet hast.",
simpleMode: "Weniger Einstellungen",
advancedMode: "Mehr Einstellungen",
settings: "Einstellungen",
help: "Hilfe",
mainMenu_lastSessions: "Letzte Sessions",
mainMenu_social: "Freunde",
mainMenu_swimNow: "Jetzt schwimmen",
}

View File

@ -0,0 +1,11 @@
export default {
connecting: "Connecting",
connectSubtext: "Please connect your phone to the WiFi of your SwimTracker",
simpleMode: "Simple Mode",
advancedMode: "Advanced Mode",
help: "Need help?",
settings: "Settings",
mainMenu_lastSessions: "Last Sessions",
mainMenu_social: "Social",
mainMenu_swimNow: "Start training now"
}

27
package-lock.json generated
View File

@ -14,6 +14,7 @@
"expo-blur": "~9.0.3", "expo-blur": "~9.0.3",
"expo-keep-awake": "~9.1.2", "expo-keep-awake": "~9.1.2",
"expo-linear-gradient": "~9.1.0", "expo-linear-gradient": "~9.1.0",
"expo-localization": "~10.1.0",
"immutable": "^4.0.0-rc.12", "immutable": "^4.0.0-rc.12",
"moment": "^2.27.0", "moment": "^2.27.0",
"msgpack-lite": "^0.1.26", "msgpack-lite": "^0.1.26",
@ -7709,6 +7710,14 @@
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-9.1.0.tgz", "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-9.1.0.tgz",
"integrity": "sha512-9W4TxaDrZUzmLzd18+Eo2Ef7ONnuLrHKX0PRyHEGY2/rJCtGiPJtJuR6AS5dkX7WB/Pxlx0zK/p/qDHHw09Uxg==" "integrity": "sha512-9W4TxaDrZUzmLzd18+Eo2Ef7ONnuLrHKX0PRyHEGY2/rJCtGiPJtJuR6AS5dkX7WB/Pxlx0zK/p/qDHHw09Uxg=="
}, },
"node_modules/expo-localization": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-10.1.0.tgz",
"integrity": "sha512-covyQ1YJneUBvjkNp0p8Lp7ej5Plz1OpJiwA2P6O3hZ4yVVqNDOOAb2WADLARPtxNJrDdntSozgI+JSz/yQxmQ==",
"dependencies": {
"rtl-detect": "^1.0.2"
}
},
"node_modules/expo-permissions": { "node_modules/expo-permissions": {
"version": "12.0.1", "version": "12.0.1",
"resolved": "https://registry.npmjs.org/expo-permissions/-/expo-permissions-12.0.1.tgz", "resolved": "https://registry.npmjs.org/expo-permissions/-/expo-permissions-12.0.1.tgz",
@ -17812,6 +17821,11 @@
"node": "6.* || >= 7.*" "node": "6.* || >= 7.*"
} }
}, },
"node_modules/rtl-detect": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.0.4.tgz",
"integrity": "sha512-EBR4I2VDSSYr7PkBmFy04uhycIpDKp+21p/jARYXlCSjQksTBQcJ0HFUPOO79EPPH5JS6VAhiIQbycf0O3JAxQ=="
},
"node_modules/run-async": { "node_modules/run-async": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
@ -25951,6 +25965,14 @@
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-9.1.0.tgz", "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-9.1.0.tgz",
"integrity": "sha512-9W4TxaDrZUzmLzd18+Eo2Ef7ONnuLrHKX0PRyHEGY2/rJCtGiPJtJuR6AS5dkX7WB/Pxlx0zK/p/qDHHw09Uxg==" "integrity": "sha512-9W4TxaDrZUzmLzd18+Eo2Ef7ONnuLrHKX0PRyHEGY2/rJCtGiPJtJuR6AS5dkX7WB/Pxlx0zK/p/qDHHw09Uxg=="
}, },
"expo-localization": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-10.1.0.tgz",
"integrity": "sha512-covyQ1YJneUBvjkNp0p8Lp7ej5Plz1OpJiwA2P6O3hZ4yVVqNDOOAb2WADLARPtxNJrDdntSozgI+JSz/yQxmQ==",
"requires": {
"rtl-detect": "^1.0.2"
}
},
"expo-permissions": { "expo-permissions": {
"version": "12.0.1", "version": "12.0.1",
"resolved": "https://registry.npmjs.org/expo-permissions/-/expo-permissions-12.0.1.tgz", "resolved": "https://registry.npmjs.org/expo-permissions/-/expo-permissions-12.0.1.tgz",
@ -33831,6 +33853,11 @@
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
"integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==" "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA=="
}, },
"rtl-detect": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.0.4.tgz",
"integrity": "sha512-EBR4I2VDSSYr7PkBmFy04uhycIpDKp+21p/jARYXlCSjQksTBQcJ0HFUPOO79EPPH5JS6VAhiIQbycf0O3JAxQ=="
},
"run-async": { "run-async": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",

View File

@ -24,6 +24,8 @@
"expo-blur": "~9.0.3", "expo-blur": "~9.0.3",
"expo-keep-awake": "~9.1.2", "expo-keep-awake": "~9.1.2",
"expo-linear-gradient": "~9.1.0", "expo-linear-gradient": "~9.1.0",
"expo-localization": "~10.1.0",
"i18n-js": "^3.8.0",
"immutable": "^4.0.0-rc.12", "immutable": "^4.0.0-rc.12",
"moment": "^2.27.0", "moment": "^2.27.0",
"msgpack-lite": "^0.1.26", "msgpack-lite": "^0.1.26",

View File

@ -30,9 +30,10 @@ export const STOP_SESSION = "STOP_SESSION";
export const WIFI_SET_STATE = "WIFI_SET_STATE"; export const WIFI_SET_STATE = "WIFI_SET_STATE";
export const reportNewWifiState = (newStateStr) => ({ export const reportNewWifiState = (newStateStr, newHostname) => ({
type: WIFI_SET_STATE, type: WIFI_SET_STATE,
newStateStr: newStateStr newStateStr: newStateStr,
newHostname: newHostname,
}); });
export const reportSessionStarted = (sessionId) => ({ export const reportSessionStarted = (sessionId) => ({
@ -83,7 +84,6 @@ export class DeviceReduxCoupling {
const state = this.reduxStore.getState(); const state = this.reduxStore.getState();
if (this.conn === null || (state.settings.swimTrackerHost != this.conn.swimTrackerHost)) { if (this.conn === null || (state.settings.swimTrackerHost != this.conn.swimTrackerHost)) {
console.log(" ---- starting websocket connection to ", state.settings.swimTrackerHost);
if( this.conn !== null) { if( this.conn !== null) {
this.conn.close(); this.conn.close();
} }
@ -92,7 +92,7 @@ export class DeviceReduxCoupling {
this._onNewData, this._onNewData,
(sessionId) => this.reduxStore.dispatch(reportSessionStarted(sessionId)), (sessionId) => this.reduxStore.dispatch(reportSessionStarted(sessionId)),
() => this.reduxStore.dispatch(reportSessionStopped()), () => this.reduxStore.dispatch(reportSessionStopped()),
(response) => this.reduxStore.dispatch(reportNewWifiState(response["state"])), (response) => this.reduxStore.dispatch(reportNewWifiState(response["state"], response["hostname"])),
() => this.reduxStore.dispatch(reportDeviceConnect()), () => this.reduxStore.dispatch(reportDeviceConnect()),
() => this.reduxStore.dispatch(reportDeviceDisconnect()) () => this.reduxStore.dispatch(reportDeviceDisconnect())
); );
@ -130,6 +130,7 @@ const INITIAL_DEVICE_STATE = {
sessionId: 0, sessionId: 0,
measurements: List(), measurements: List(),
analysis: INITIAL_ANALYSIS, analysis: INITIAL_ANALYSIS,
deviceReportedHostname: "",
}; };
export const deviceStateReducer = (state = INITIAL_DEVICE_STATE, action) => { export const deviceStateReducer = (state = INITIAL_DEVICE_STATE, action) => {
@ -162,7 +163,7 @@ export const deviceStateReducer = (state = INITIAL_DEVICE_STATE, action) => {
if (action.newStateStr === "STATION_MODE") { wifiState = WifiState.STA; } if (action.newStateStr === "STATION_MODE") { wifiState = WifiState.STA; }
else if (action.newStateStr === "AP_PROVISIONING") { wifiState = WifiState.AP_PROVISIONING; } else if (action.newStateStr === "AP_PROVISIONING") { wifiState = WifiState.AP_PROVISIONING; }
else if (action.newStateStr === "AP_SECURE") { wifiState = WifiState.AP_SECURE; } else if (action.newStateStr === "AP_SECURE") { wifiState = WifiState.AP_SECURE; }
return { ...state, wifiState: wifiState }; return { ...state, wifiState: wifiState, deviceReportedHostname: action.newHostname };
default: default:
//console.log("Unhandled state in deviceStateReducer", action, action.type, "state", state); //console.log("Unhandled state in deviceStateReducer", action, action.type, "state", state);
return state; return state;

View File

@ -10,7 +10,7 @@ export const changeUsername = newUsername => ({
newUserName: newUsername, newUserName: newUsername,
}); });
export const changeSwimTrackerHostname = newSwimTrackerHost => ( { export const changeSwimTrackerHostname = newSwimTrackerHost => ({
type: CHANGE_SWIMTRACKER_HOSTNAME, type: CHANGE_SWIMTRACKER_HOSTNAME,
newSwimTrackerHost: newSwimTrackerHost, newSwimTrackerHost: newSwimTrackerHost,
}); });
@ -34,7 +34,7 @@ const INITIAL_SETTINGS = {
windowSizeInSecs: 5, windowSizeInSecs: 5,
numMeasurementsPerSec: 10, numMeasurementsPerSec: 10,
kgFactor: 1.0 / 701.0, kgFactor: 1.0 / 701.0,
peakDetector: 'SIMPLE', // either 'SIMPLE' or 'ZSCORE' peakDetector: 'SIMPLE', // either 'SIMPLE' or 'ZSCORE'
peakDetectorSimpleThreshold: 2000, peakDetectorSimpleThreshold: 2000,
@ -44,17 +44,17 @@ const INITIAL_SETTINGS = {
peakDetectorZScoreInfluence: 0.1, peakDetectorZScoreInfluence: 0.1,
activeTimeThreshold: 700, activeTimeThreshold: 700,
movingAverageWindowSize: 10*3, movingAverageWindowSize: 10 * 3,
} }
}; };
const settingsReducer = (state = INITIAL_SETTINGS, action) => { const settingsReducer = (state = INITIAL_SETTINGS, action) => {
switch (action.type) { switch (action.type) {
case CHANGE_USER_NAME: case CHANGE_USER_NAME:
return { ...state, username: action.newUsername }; return { ...state, username: action.newUsername };
case CHANGE_SWIMTRACKER_HOSTNAME: case CHANGE_SWIMTRACKER_HOSTNAME:
return {... state, swimTrackerHost: action.newSwimTrackerHost}; return { ...state, swimTrackerHost: action.newSwimTrackerHost };
default: default:
return state; return state;
} }

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useState } from "react";
import { import {
StyleSheet, StyleSheet,
Text, Text,
@ -8,37 +8,59 @@ import {
} from "react-native"; } from "react-native";
import SetupView from '../components/SetupView'; import SetupView from '../components/SetupView';
import EvilIcon from "react-native-vector-icons/EvilIcons";
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { changeSwimTrackerHostname } from '../state/Reducer'; import { changeSwimTrackerHostname } from '../state/Reducer';
import i18n from 'i18n-js';
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) { function ConnectingView(props) {
const [isHostnameValid, setIsHostnameValid] = useState(isValidHostname(props.swimTrackerHost));
const [hostnameTextInput, setHostnameTextInput] = useState(props.swimTrackerHost);
const [advancedMode, setAdvancedMode] = useState(false);
let onHostnameChange = newHostName => { let onHostnameChange = newHostName => {
props.dispatch(changeSwimTrackerHostname(newHostName)); setHostnameTextInput(newHostName);
const newHostnameValid = isValidHostname(newHostName);
setIsHostnameValid(newHostnameValid);
if (newHostnameValid) {
props.dispatch(changeSwimTrackerHostname(newHostName));
}
return true;
}; };
const hiddenStyle = advancedMode ? {} : {"display": "none"};
return ( return (
<SetupView <SetupView
headerText="Connecting..." headerText={i18n.t("connecting") + "..."}
lowerLeftButtonText="Advanced Setup" lowerLeftButtonText={ i18n.t(advancedMode ? 'simpleMode' : 'advancedMode') }
lowerRightButtonText="Need help?" onLowerLeftButtonPress={() => { setAdvancedMode(!advancedMode); }}
lowerRightButtonText={i18n.t('help')}
> >
<View style={{flexDirection: "row", alignItems: "center"}}>
<ActivityIndicator size="large" color="#ffffff" /> <ActivityIndicator size="large" color="#ffffff" />
<Text style={styles.subtext}> <Text style={styles.subtext}>
Please connect your phone to the WiFi of your SwimTracker {i18n.t('connectSubtext')}
</Text> </Text>
<View style={styles.row}>
<EvilIcon name="archive" style={styles.hostIcon}></EvilIcon>
<TextInput
onChangeText={onHostnameChange}
value={props.swimTrackerHost}
style={styles.hostnameInput}
placeholder="Hostname/IP"
placeholderTextColor="rgba(255,255,255,0.5)"
></TextInput>
</View> </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> </SetupView>
) )
} }
@ -46,32 +68,33 @@ function ConnectingView(props) {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
subtext: { subtext: {
color: "rgba(255,255,255,1)", color: "rgba(255,255,255,1)",
textAlign: "center", textAlign: "left",
fontSize: 18, fontSize: 16,
lineHeight: 25, lineHeight: 25,
width: "80%", width: "100%",
//paddingTop: 50,
paddingLeft: 30,
}, },
row: { row: {
backgroundColor: "rgba(255,255,255,0.4)", backgroundColor: "rgba(255,255,255,0.4)",
borderRadius: 5, borderRadius: 5,
width: "80%", width: "100%",
height: 50, height: 50,
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
marginTop: 60, marginTop: 60,
marginBottom: 5, marginBottom: 5,
}, },
hostIcon: {
fontSize: 25,
color: "rgba(255,255,255,1)",
marginLeft: 15,
marginRight: 15,
},
hostnameInput: { hostnameInput: {
height: 30, height: 30,
color: "rgba(255,255,255,1)", color: "rgba(255,255,255,1)",
width: "80%", width: "80%",
fontSize: 18, fontSize: 18,
},
label : {
color: "rgb(80, 80, 80)",
marginLeft: 15,
marginRight: 10,
} }
}); });

View File

@ -248,7 +248,7 @@ class LastSessionsView extends React.Component {
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<StatusBar hidden={true} /> <StatusBar barStyle="light-content" backgroundColor="rgba(0,0,0,0.4)" translucent={true} />
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<ImageHeader <ImageHeader
text="LETZTE SESSIONS" text="LETZTE SESSIONS"

View File

@ -12,10 +12,8 @@ import MaterialIcon from "react-native-vector-icons/MaterialIcons";
import MaterialCommIcon from "react-native-vector-icons/MaterialCommunityIcons"; import MaterialCommIcon from "react-native-vector-icons/MaterialCommunityIcons";
import EntypoIcon from "react-native-vector-icons/Entypo"; import EntypoIcon from "react-native-vector-icons/Entypo";
import FeatherIcon from "react-native-vector-icons/Feather";
//import { MaterialCommunityIcons } from '@expo/vector-icons';
import i18n from 'i18n-js';
import { ConnState, startSession } from '../state/DeviceReduxCoupling'; import { ConnState, startSession } from '../state/DeviceReduxCoupling';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -41,7 +39,6 @@ const largeHeaderStyles = StyleSheet.create({
alignItems: "center", alignItems: "center",
justifyContent: "flex-end", justifyContent: "flex-end",
paddingBottom: 10, paddingBottom: 10,
//alignContent: "space-between"
}, },
titleText: { titleText: {
color: "rgba(255,255,255,1)", color: "rgba(255,255,255,1)",
@ -72,17 +69,18 @@ function ButtonGrid(props) {
activeOpacity={0.6} activeOpacity={0.6}
> >
<MaterialCommIcon name="swim" style={buttonGridStyles.icon}></MaterialCommIcon> <MaterialCommIcon name="swim" style={buttonGridStyles.icon}></MaterialCommIcon>
<Text style={buttonGridStyles.buttonText}>{"LETZTE\nSESSIONS"}</Text> <Text style={buttonGridStyles.buttonText}>{ i18n.t('mainMenu_lastSessions').toUpperCase().split(" ").join("\n") }</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={buttonGridStyles.columnContainer}> <View style={buttonGridStyles.columnContainer}>
<TouchableOpacity <TouchableOpacity
onPress={props.onSocialPress} onPress={props.onSocialPress}
style={[{ backgroundColor: themeColors["MIDNIGHT BLUE"] }, buttonGridStyles.button]} style={[{ backgroundColor: "#444" }, buttonGridStyles.button]}
activeOpacity={0.6} activeOpacity={0.6}
disabled={true}
> >
<MaterialCommIcon name="account-group" style={buttonGridStyles.icon}></MaterialCommIcon> <MaterialCommIcon name="account-group" style={buttonGridStyles.icon}></MaterialCommIcon>
<Text style={buttonGridStyles.buttonText}>SOCIAL</Text> <Text style={buttonGridStyles.buttonText}>{ i18n.t('mainMenu_social').toUpperCase()}</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
onPress={props.onSettingsPress} onPress={props.onSettingsPress}
@ -90,7 +88,7 @@ function ButtonGrid(props) {
activeOpacity={0.6} activeOpacity={0.6}
> >
<MaterialIcon name="settings" style={buttonGridStyles.icon}></MaterialIcon> <MaterialIcon name="settings" style={buttonGridStyles.icon}></MaterialIcon>
<Text style={buttonGridStyles.buttonText}>EINSTELLUNGEN</Text> <Text style={buttonGridStyles.buttonText}>{ i18n.t('settings').toUpperCase()}</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
@ -193,7 +191,7 @@ const fullWidthButtonStyles = StyleSheet.create({
function MainMenuView(props) { function MainMenuView(props) {
const s = props.connState; const s = props.connState;
let startButtonText = "JETZT SCHWIMMEN"; let startButtonText = i18n.t('mainMenu_swimNow').toUpperCase();
let startButtonDisabled = false; let startButtonDisabled = false;
if (s === ConnState.DISCONNECTED) { if (s === ConnState.DISCONNECTED) {
startButtonText = "NICHT VERBUNDEN"; startButtonText = "NICHT VERBUNDEN";

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { import {
StyleSheet, StyleSheet,
View, View,
@ -12,6 +12,9 @@ import MaterialIcon from "react-native-vector-icons/MaterialCommunityIcons";
import EntypoIcon from "react-native-vector-icons/Entypo"; import EntypoIcon from "react-native-vector-icons/Entypo";
import ImageHeader from "../components/ImageHeader"; import ImageHeader from "../components/ImageHeader";
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { TouchableOpacity } from "react-native-gesture-handler";
import request from '../utility/PromiseRequest';
import i18n from 'i18n-js';
// --------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------
@ -51,6 +54,29 @@ function SettingsSwitch(props) {
) )
} }
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) { function SettingsSlider(props) {
/* /*
<Slider <Slider
@ -136,20 +162,65 @@ const settingsGroupStyles = StyleSheet.create({
switch: { switch: {
//minHeight: 50, //minHeight: 50,
//minWidth: 80, //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" });
return result;
}
function SettingsView(props) { function SettingsView(props) {
console.log("settings props", 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 ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<StatusBar hidden={true} /> <StatusBar barStyle="light-content" backgroundColor="rgba(0,0,0,0.4)" translucent={true} />
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<ImageHeader <ImageHeader
text="EINSTELLUNGEN" text={i18n.t('settings').toUpperCase()}
navigation={props.navigation} navigation={props.navigation}
image={require("../assets/infinity_pool2.jpg")} image={require("../assets/infinity_pool2.jpg")}
/> />
@ -163,6 +234,11 @@ function SettingsView(props) {
/> />
<SettingsSwitch label="Start automatically" /> <SettingsSwitch label="Start automatically" />
<SettingsSwitch label="Stop 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> </SettingsGroup>
</View> </View>
</View> </View>

View File

@ -9,6 +9,8 @@ import {
} from "react-native"; } from "react-native";
import SetupView from '../components/SetupView'; import SetupView from '../components/SetupView';
import MaterialIcon from "react-native-vector-icons/MaterialCommunityIcons"; import MaterialIcon from "react-native-vector-icons/MaterialCommunityIcons";
import { connect } from 'react-redux';
import { changeSwimTrackerHostname } from '../state/Reducer';
function WifiListElement(props) { function WifiListElement(props) {
@ -129,7 +131,11 @@ class WifiSelectionView extends React.Component {
buttonText: "OK", buttonText: "OK",
subText: "Please enter the password for your home WiFi", subText: "Please enter the password for your home WiFi",
onSubmit: (ssid, pw) => { onSubmit: (ssid, pw) => {
console.log("1");
this.props.device.conn.wifiSetModeSTA(ssid, pw); 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");
}, },
}); });
}}> }}>
@ -181,4 +187,8 @@ const styles = StyleSheet.create({
} }
}); });
export default WifiSelectionView; const mapStateToProps = (state) => {
return { deviceReportedHostname: state.deviceState.deviceReportedHostname };
};
export default connect(mapStateToProps)(WifiSelectionView);

8657
yarn.lock Normal file

File diff suppressed because it is too large Load Diff