Compare commits

...

10 Commits

Author SHA1 Message Date
Martin Bauer 96db91f027 Fixed crash on android in view sessions
- time formatting wasn't defined
- fixes issue 6
2023-10-29 12:55:13 +01:00
Martin Bauer 6ed968a8c5 Small fixes in views 2023-10-28 14:07:27 +02:00
Martin Bauer 977d4bcbc9 removed splashscreen dependency (deprecated) 2023-10-28 14:06:08 +02:00
Martin Bauer 3c62e1e04b doc files and deployment 2023-10-28 14:05:54 +02:00
Martin Bauer 52ddff10f0 Fixing websocket issue on android by adding usesCleartextTraffic in app.config.js 2023-10-28 14:05:34 +02:00
Martin Bauer 05681b2a5a last session view: correct ordering from new to old 2023-10-28 14:01:42 +02:00
Martin Bauer a576e91290 LastSessionsView
- added missing localization
- no reload on delete
2023-10-02 09:41:48 +02:00
Martin Bauer 9b6bb7f126 New setup from scratch - all modules updated - app now in subfolder 2023-09-29 22:04:52 +02:00
Martin Bauer e28ab91935 Local changes from desktop 2023-07-16 13:22:34 +02:00
Martin Bauer 0d34959e08 backend changes 2021-08-29 17:57:25 +02:00
78 changed files with 9851 additions and 44799 deletions

View File

@ -1 +0,0 @@
{}

15
.expo/README.md Normal file
View File

@ -0,0 +1,15 @@
> Why do I have a folder named ".expo" in my project?
The ".expo" folder is created when an Expo project is started using "expo start" command.
> What do the files contain?
- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
- "packager-info.json": contains port numbers and process PIDs that are used to serve the application to the mobile device/simulator.
- "settings.json": contains the server configuration that is used to serve the application manifest.
> Should I commit the ".expo" folder?
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.

8
.expo/settings.json Normal file
View File

@ -0,0 +1,8 @@
{
"hostType": "lan",
"lanType": "ip",
"dev": true,
"minify": false,
"urlRandomness": null,
"https": false
}

13
.gitignore vendored
View File

@ -1,13 +0,0 @@
node_modules/**/*
.expo/*
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
web-report/
/dist
/venv

View File

@ -1 +0,0 @@
{}

3
SwimTracker/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/node_modules
/.expo
.directory

View File

@ -1,5 +1,5 @@
import React from 'react';
import AppLoading from 'expo-app-loading';
import * as SplashScreen from 'expo-splash-screen';
import { Ionicons } from '@expo/vector-icons';
import * as Font from 'expo-font';
@ -13,12 +13,6 @@ import { persistStore, persistReducer } from 'redux-persist'
import hardSet from 'redux-persist/lib/stateReconciler/hardSet'
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
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
@ -32,16 +26,6 @@ import ConnectingView from './views/ConnectingView';
import WifiSelectionView from './views/WifiSelectionView';
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 = Localization.locale; // Set the locale once at the beginning of your app.
//i18n.locale = "en-US";
i18n.fallbacks = true; // When a value is missing from a language it'll fallback to another language with the key present.
const persistConfig = {
key: 'root',
storage: AsyncStorage,
@ -53,6 +37,9 @@ const store = createStore(persistedReducer);
const persistor = persistStore(store);
const Stack = createStackNavigator();
// Keep the splash screen visible while we fetch resources
SplashScreen.preventAutoHideAsync();
export default class App extends React.Component {
constructor(props) {
super(props);
@ -86,9 +73,10 @@ export default class App extends React.Component {
}
render() {
if (!this.state.isReady) {
return <AppLoading />;
if(this.state.isReady) {
SplashScreen.hideAsync();
}
const screenOptions = {
headerShown: false,
};
@ -153,8 +141,8 @@ export default class App extends React.Component {
return (
<Provider store={store}>
<PersistGate loading={<AppLoading />} persistor={persistor}>
<NavigationContainer>
<PersistGate persistor={persistor}>
<NavigationContainer >
<Stack.Navigator >
{activeView}
</Stack.Navigator>

45
SwimTracker/app.config.js Normal file
View File

@ -0,0 +1,45 @@
let config = {
"expo": {
"name": "SwimTracker",
"slug": "SwimTracker",
"version": "1.0.1",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "cover",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": false,
"icon": "./assets/icon-ios.png",
"bundleIdentifier": "tech.bauer.swimtracker"
},
"android": {
"package": "tech.bauer.swimtracker",
"permissions": [ "INTERNET" ]
},
"web": {
"favicon": "./assets/favicon.png"
},
"plugins": [
"expo-localization",
["expo-build-properties", {
"android": {
"usesCleartextTraffic": true
}
}]
],
"extra": {
"eas": {
"projectId": "04899022-7d11-43b0-a31a-394a62339352"
}
}
}
};
module.exports = config;

View File

@ -0,0 +1,17 @@
> Why do I have a folder named ".expo" in my project?
The ".expo" folder is created when an Expo project is started using "expo start" command.
> What does the "packager-info.json" file contain?
The "packager-info.json" file contains port numbers and process PIDs that are used to serve the application to the mobile device/simulator.
> What does the "settings.json" file contain?
The "settings.json" file contains the server configuration that is used to serve the application manifest.
> Should I commit the ".expo" folder?
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 419 KiB

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 253 KiB

View File

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 340 KiB

View File

Before

Width:  |  Height:  |  Size: 602 KiB

After

Width:  |  Height:  |  Size: 602 KiB

View File

Before

Width:  |  Height:  |  Size: 740 KiB

After

Width:  |  Height:  |  Size: 740 KiB

View File

Before

Width:  |  Height:  |  Size: 899 KiB

After

Width:  |  Height:  |  Size: 899 KiB

View File

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

109
SwimTracker/cli/index.js Normal file
View File

@ -0,0 +1,109 @@
import SwimTrackerWebsocketConnection from "../data_processing/SwimTrackerWebsocketConnection.js";
import yargs from 'yargs';
import WS from 'ws';
import { SerialPort } from 'serialport'
import { ReadlineParser } from '@serialport/parser-readline'
import chalk from 'chalk';
/**
* Opens serial port and calls provided function on each line
*
* @param {string} serialPortPath
* @param {(str) => any} onLineReceived
*/
function openSerialConnection(serialPortPath, onLineReceived) {
const port = new SerialPort({
path: serialPortPath,
baudRate: 115200
});
const parser = port.pipe(new ReadlineParser());
parser.on('data', onLineReceived);
}
function basicTest(deviceIpOrHostname, serialPort) {
let lastHostLog = undefined;
let lastDeviceLog = undefined;
function log(color, main, startCol) {
if(lastHostLog === undefined)
lastHostLog = Date.now();
const currentTime = Date.now();
const dt = currentTime - lastHostLog;
lastHostLog = currentTime;
console.log(color((startCol).padEnd(9), "|", String(dt).padStart(5), "|", main));
}
function logDeviceTime(color, main, column1, time) {
if(lastDeviceLog === undefined)
lastDeviceLog = time;
const dt = time - lastDeviceLog;
lastDeviceLog = time;
console.log(color((column1).padEnd(9), "|", String(dt).padStart(5), "|", main));
}
const onStarted = (sessionid) => { log(chalk.yellow, "Session started " + sessionid, "WS Msg") };
const onStopped = () => { log(chalk.yellow, "Session stopped", "WS Msg") };
const onWifiStateInfo = (wifiInfo) => { log(chalk.cyan, JSON.stringify(wifiInfo), "WS Wifi") };
const onWebsocketLog = (logMsg) => {logDeviceTime(chalk.blue, logMsg.msg, "WS Log", String(logMsg.time))};
const onDisconnect = () => { log(chalk.red, "Disconnected", "WS Msg") };
const onConnect = () => {
log(chalk.blue,"Connected to device", "WS Msg")
conn.sendLogStreamStartCommand();
};
let data = [];
const onNewMeasurementData = (newDataAsUint16Arr) => {
const arr = Array.from(newDataAsUint16Arr);
newDataAsUint16Arr.forEach(element => {
data.push(element);
});
const existing = "(" + String(data.length).padStart(5) + ") ";
if(arr.length > 5) {
log(chalk.yellow, existing + "Bulk " + arr.length + " measurements", "WS Data");
} else {
log(chalk.gray, existing + JSON.stringify(Array.from(newDataAsUint16Arr)), "WS Data");
}
};
if(serialPort) {
openSerialConnection(serialPort, (receivedLine) => {
log(chalk.gray, receivedLine, "Serial");
});
}
const conn = new SwimTrackerWebsocketConnection(deviceIpOrHostname, onNewMeasurementData, onStarted, onStopped,
onWifiStateInfo, onConnect, onDisconnect,
{ WebSocket: WS });
conn.onLogMessage = onWebsocketLog;
}
basicTest("192.168.178.56", "/dev/ttyUSB1");
//basicTest("192.168.178.56", undefined);
// {"speeds":[0.4, -0.3],"durations":[1500, 1500]}
/*
Testrun:
Captures:
- streamed data from websocket
- log messages from serial port
- output of serial port
File format:
- timestamp of aquisition
- type
- websocket message
- serial port line
Logging:
- websocket errors always
Commands:
- replay file
- data as csv
*/

View File

@ -22,7 +22,7 @@ const IconCard = props => {
<Text style={{ color: 'white', fontSize: props.fontSize, textAlign: "center" }}> {props.value}</Text>
</View>
<View style={{ alignItems: 'center', justifyContent: 'center', paddingLeft: 20 }}>
<IconClass style={{ color: 'white', fontSize: 40 }} name={props.iconName} />
<IconClass style={{ color: 'white', fontSize: 30 }} name={props.iconName} />
<Text style={{ color: 'white', marginTop: 5 }}> {props.label}</Text>
</View>
</View>

View File

@ -15,6 +15,7 @@ const OpCodes = {
WIFI_STATE_RESPONSE: 8,
WIFI_SCAN_RESPONSE: 9,
APP_LAYER_PING: 10,
LOG_UPDATE: 11,
// from frontend to device
START_SESSION: 128,
@ -25,13 +26,26 @@ const OpCodes = {
WIFI_STATE_SET: 133,
WIFI_STATE_GET: 134,
WIFI_TRIGGER_SCAN: 135,
LOG_STREAMING_START: 136,
LOG_STREAMING_STOP: 137
};
const HEARTBEAT_TIMEOUT = 3000;
const PROVISIONING_IP = "192.168.42.1";
export default class SwimTrackerWebsocketConnection {
constructor(swimTrackerHost, onData, onStarted, onStopped, onWifiStateInfo, onConnect, onDisconnect) {
/**
* Creates a new persistent websocket connection to a swimtracker device
*
* @param {string} swimTrackerHost hostname or ip of the swimtracker device
* @param {(data: Uint16Array) => any} onData called whenever new measurement data is available
* @param {(sessionId: number) => any} onStarted called when a new measurement session was started
* @param {() => any} onStopped called when session was stopped
* @param {(wifistate : object) => any} onWifiStateInfo wifi state contains "state" (STATION_MODE|AP_PROVISIONING|AP_SECURE) and "hostname"
* @param {() => any} onConnect called when websocket connection was established
* @param {() => any} onDisconnect called when websocket disconnected
*/
constructor(swimTrackerHost, onData, onStarted, onStopped, onWifiStateInfo, onConnect, onDisconnect, reconnectingWsOptions={}) {
this.swimTrackerHost = swimTrackerHost;
this.onData = onData;
@ -40,14 +54,14 @@ export default class SwimTrackerWebsocketConnection {
this.onWifiStateInfo = onWifiStateInfo;
this.onConnect = onConnect;
this.onDisconnect = onDisconnect;
this.onLogMessage = () => {};
// 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(urlProvider, [], { maxReconnectionDelay: 3000 });
this.ws = new ReconnectingWebSocket(urlProvider, [], { ...reconnectingWsOptions, maxReconnectionDelay: 3000 });
this.ws.onmessage = this._onMessage;
this.ws.onopen = this.onConnect;
this.ws.onclose = this.onDisconnect;
@ -98,6 +112,14 @@ export default class SwimTrackerWebsocketConnection {
this._sendMsg(OpCodes.TARE);
}
sendLogStreamStartCommand = () => {
this._sendMsg(OpCodes.LOG_STREAMING_START);
}
sendLogStreamStopCommand = () => {
this._sendMsg(OpCodes.LOG_STREAMING_STOP);
}
scanWifiNetworks() {
console.log("Trigger wifi scan");
this._sendMsg(OpCodes.WIFI_TRIGGER_SCAN);
@ -177,12 +199,15 @@ export default class SwimTrackerWebsocketConnection {
} else if (opCode === OpCodes.WIFI_STATE_RESPONSE) {
const wifiInfo = msgpack.decode(payload, { codec: this.msgpackCodec });
this.onWifiStateInfo(wifiInfo);
} else if (opCode === OpCodes.LOG_UPDATE) {
const logMsg = msgpack.decode(payload, { codec: this.msgpackCodec });
this.onLogMessage(logMsg);
} else if (opCode === OpCodes.APP_LAYER_PING) {
//console.log("got heartbeat");
}
}
_onError = (ev) => {
console.log("Websocket error", ev);
console.log("Websocket error", ev, ev.error, ev.message);
}
};

28
SwimTracker/eas.json Normal file
View File

@ -0,0 +1,28 @@
{
"cli": {
"version": ">= 5.4.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"preview": {
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"production": {
"android": {
"buildType": "apk"
}
}
},
"submit": {
"production": {}
}
}

View File

@ -0,0 +1,14 @@
// This file was generated by "npx expo customize metro.config.js"
// to fix an error in make-plural (from i18n) when building for android
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
// I added this line, the rest was already there, after the command run
config.watcher.additionalExts.push('mjs', 'cjs');
module.exports = config;

46
SwimTracker/package.json Normal file
View File

@ -0,0 +1,46 @@
{
"name": "swimtracker",
"version": "1.0.0",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"@expo/webpack-config": "^19.0.0",
"@react-native-async-storage/async-storage": "1.18.2",
"@react-navigation/native": "^6.1.8",
"@react-navigation/stack": "^6.3.18",
"expo": "~49.0.13",
"expo-build-properties": "~0.8.3",
"expo-dev-client": "~2.4.11",
"expo-localization": "~14.3.0",
"expo-splash-screen": "~0.20.5",
"expo-status-bar": "~1.6.0",
"i18n-js": "^4.3.2",
"immutable": "^5.0.0-beta.4",
"moment": "^2.29.4",
"msgpack-lite": "^0.1.26",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.72.6",
"react-native-gesture-handler": "~2.12.0",
"react-native-safe-area-context": "4.6.3",
"react-native-screens": "~3.22.0",
"react-native-svg": "13.9.0",
"react-native-swipe-list-view": "^3.2.9",
"react-native-web": "~0.19.6",
"react-redux": "^8.1.2",
"react-xml-parser": "^1.1.8",
"reconnecting-websocket": "^4.4.0",
"redux": "^4.2.1",
"redux-persist": "^6.0.0",
"websocket": "^1.0.34"
},
"devDependencies": {
"@babel/core": "^7.20.0"
},
"private": true
}

View File

@ -0,0 +1,66 @@
import {i18n} from './i18n';
const locale = i18n.locale;
const fullDateFormat = Intl.DateTimeFormat(locale, {
weekday: "short",
year: "2-digit",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
const timeFormatter = Intl.DateTimeFormat(locale, {
hour: "2-digit",
minute: "2-digit",
});
/*
// Doesn't work on android yet (so use full time for now instead of relative time)
const relativeTimeFormatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto"});
const DIVISIONS = [
{ amount: 60, name: "seconds" },
{ amount: 60, name: "minutes" },
{ amount: 24, name: "hours" },
{ amount: 7, name: "days" },
{ amount: 4.34524, name: "weeks" },
{ amount: 12, name: "months" },
{ amount: Number.POSITIVE_INFINITY, name: "years" },
];
function formatTimeAgo(date) {
let duration = (date - new Date()) / 1000
for (let i = 0; i < DIVISIONS.length; i++) {
const division = DIVISIONS[i]
if (Math.abs(duration) < division.amount) {
const suffix = (division.name === "days") ? ", " + timeFormatter.format(date) : "";
return relativeTimeFormatter.format(Math.round(duration), division.name) + suffix
}
duration /= division.amount
}
}
*/
function timeSince(timeStamp) {
const now = Math.floor((new Date()).getTime() / 1000);
const secondsPassed = now - timeStamp;
const daysPassed = secondsPassed / 60 / 60 / 24;
const timeStampDate = new Date(timeStamp * 1000);
//if (daysPassed < 2)
// return formatTimeAgo(timeStampDate);
//else
return fullDateFormat.format(timeStampDate);
}
const toTimeStr = seconds => {
let minuteStr = String(Math.floor(seconds / 60));
if (minuteStr.length < 2)
minuteStr = "0" + minuteStr;
let secondStr = String(Math.floor(seconds % 60));
if (secondStr.length < 2)
secondStr = "0" + secondStr;
return minuteStr + ":" + secondStr;
}
export { toTimeStr, timeSince };

View File

@ -0,0 +1,36 @@
import { I18n } from "i18n-js";
import * as Localization from 'expo-localization';
const translation_store = {
en: {
connecting: "Connecting",
connectSubtext: "Please connect your phone to the WiFi of your SwimTracker",
simpleMode: "Simple Mode",
advancedMode: "Advanced Mode",
help: "Need help?",
settings: "Settings",
lastSessions: "Last Sessions",
mainMenu_social: "Social",
mainMenu_swimNow: "Swim now",
delete_session_button: "Delete",
},
de : {
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",
lastSessions: "Letzte Sessions",
mainMenu_social: "Freunde",
mainMenu_swimNow: "Jetzt schwimmen",
delete_session_button: "Löschen",
}
}
export const i18n = new I18n();
i18n.store(translation_store);
i18n.defaultLocale = "en";
i18n.enableFallback = true;
i18n.locale = Localization.locale;

View File

@ -10,7 +10,7 @@ import {
import SetupView from '../components/SetupView';
import { connect } from 'react-redux';
import { changeSwimTrackerHostname } from '../state/Reducer';
import i18n from 'i18n-js';
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*$)/;

View File

@ -20,7 +20,7 @@ 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 'i18n-js';
import {i18n} from '../utility/i18n';
function SessionCard(props) {
@ -55,7 +55,7 @@ function SessionCardBehindSwipe(props) {
style={sessionCardStyles.deleteButton}
onPress={props.onDelete}
>
<Text style={{ fontSize: 18, color: "white" }}>Löschen</Text>
<Text style={{ fontSize: 18, color: "white" }}>{i18n.t('delete_session_button')}</Text>
</TouchableOpacity>
</View>
);
@ -137,9 +137,6 @@ 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) {
@ -183,7 +180,8 @@ async function getFullData(swimTrackerHost, analysisSettings) {
const da = new DataAnalysis();
e.analysis = da.analyze(analysisSettings, e.startTime, e.values);
}
return parsed;
//console.log("full data", parsed);
return parsed.reverse();
}
// ---------------------------------------------------------------------------------------------
@ -203,9 +201,11 @@ class LastSessionsView extends React.Component {
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 filteredSession = this.state.sessions.filter((element) => element.name != sessionFileName);
this.setState({ sessions: filteredSession });
request({ url: "http://" + this.props.swimTrackerHost + "/webdav/" + sessionFileName, method: "DELETE" })
.then((value) => console.log("Successfully deleted", sessionFileName, value))
.catch((err) => console.error("Failed to delete", sessionFileName, err));
};
const onRefresh = async () => {
@ -221,13 +221,13 @@ class LastSessionsView extends React.Component {
style={{ width: "100%" }}
keyExtractor={item => item.startTime.toString()}
disableRightSwipe={true}
data={this.state.sessions.reverse()}
data={this.state.sessions}
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} />
activeTime={Math.round(data.item.analysis.activeTime / 60, 0)} />
)}
renderHiddenItem={(data, rowMap) => <SessionCardBehindSwipe onDelete={() => { deleteSession(data.item.name) }} />}
leftOpenValue={0}

View File

@ -13,7 +13,7 @@ import MaterialCommIcon from "react-native-vector-icons/MaterialCommunityIcons";
import EntypoIcon from "react-native-vector-icons/Entypo";
import i18n from 'i18n-js';
import { i18n } from '../utility/i18n';
import { ConnState, startSession } from '../state/DeviceReduxCoupling';
import { connect } from 'react-redux';
@ -113,7 +113,7 @@ const buttonGridStyles = StyleSheet.create({
},
button: {
flex: 1,
margin: 20,
margin: 14,
padding: 20,
width: 120,
height: 130,
@ -125,7 +125,7 @@ const buttonGridStyles = StyleSheet.create({
buttonText: {
color: "rgba(255,255,255,1)",
textAlign: "center",
fontSize: 16,
fontSize: 14,
},
icon: {
color: "rgba(255,255,255,1)",
@ -203,7 +203,7 @@ function MainMenuView(props) {
}
const onStartButtonPress = () => {
if (!props.connState !== ConnState.CONNECTED_RUNNING) {
if (props.connState !== ConnState.CONNECTED_RUNNING) {
props.dispatch(startSession());
}
props.navigation.navigate('Training')

View File

@ -8,13 +8,11 @@ import {
Switch,
} from "react-native";
import themeColors from '../components/themeColors';
import MaterialIcon from "react-native-vector-icons/MaterialCommunityIcons";
import EntypoIcon from "react-native-vector-icons/Entypo";
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 'i18n-js';
import { i18n } from '../utility/i18n';
// ---------------------------------------------------------------------------------------------
@ -193,6 +191,7 @@ async function queryDeviceFirmwareVersion(swimTrackerHost) {
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;
}

9067
SwimTracker/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +0,0 @@
{
"expo": {
"name": "SwimTracker",
"slug": "swimtracker",
"privacy": "public",
"platforms": [
"ios",
"android",
"web"
],
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "cover",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": false,
"icon": "./assets/icon-ios.png",
"bundleIdentifier": "tech.bauer.swimtracker"
},
"description": "",
"android": {
"package": "tech.bauer.swimtracker"
}
}
}

View File

@ -1,2 +0,0 @@
DATABASE_URL = "sqlite:///db.sqlite"
JWT_SECRET = "4SmRyfsvG86R9jZQfTshfoDlcxYlueHmkMXJbszp"

View File

@ -1,21 +0,0 @@
import databases
import sqlalchemy
from starlette import requests
from config import DATABASE_URL
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
from sqlalchemy.orm import sessionmaker
from starlette.requests import Request
database = databases.Database(DATABASE_URL)
Base: DeclarativeMeta = declarative_base()
engine = sqlalchemy.create_engine(
DATABASE_URL, connect_args={"check_same_thread": False}
)
DbSession = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db(request: Request):
return request.state.db

View File

@ -1,38 +0,0 @@
from fastapi import FastAPI
from users import add_user_routers, User
from db import database, engine, Base, DbSession
from starlette.requests import Request
from routes import router as api_router
import models
def get_app() -> FastAPI:
application = FastAPI(title="swimtracker", debug=True, version="0.1")
application.include_router(api_router)
add_user_routers(application)
return application
app = get_app()
@app.middleware("http")
async def db_session_middleware(request: Request, call_next):
request.state.db = DbSession()
response = await call_next(request)
request.state.db.close()
return response
@app.on_event("startup")
async def startup() -> None:
print("creating")
Base.metadata.create_all(engine)
if not database.is_connected:
await database.connect()
@app.on_event("shutdown")
async def shutdown() -> None:
if database.is_connected:
await database.disconnect()

View File

@ -1,42 +0,0 @@
from db import Base
from sqlalchemy import Column, Integer, Index, LargeBinary, ForeignKey, and_, or_
from typing import Tuple
class Session(Base):
__tablename__ = "session"
device_id = Column(Integer, primary_key=True)
start_time = Column(Integer, primary_key=True)
data = Column(LargeBinary(1024 * 1024 * 2), nullable=False)
user = Column(ForeignKey("user.id"), nullable=False)
value_right_shift = Column(Integer)
tare_value = Column(Integer)
kg_factor = Column(Integer)
Index('device_id', 'start_time', unique=True)
class FriendRequest(Base):
__tablename__ = "friend_request"
requesting_user = Column(ForeignKey("user.id"), primary_key=True)
receiving_user = Column(ForeignKey("user.id"), primary_key=True)
class Friendship(Base):
__tablename__ = "friendship"
user_id = Column(ForeignKey("user.id"), primary_key=True)
friend_id = Column(ForeignKey("user.id"), primary_key=True)
@staticmethod
def befriend(db, userid1, userid2):
db.add(Friendship(user_id=userid1, friend_id=userid2))
@staticmethod
def are_friends(db, userid1, userid2):
query_filter = or_(
and_(Friendship.user_id == userid1, Friendship.friend_id == userid2),
and_(Friendship.user_id == userid2, Friendship.friend_id == userid1),
)
return db.query(Friendship).filter(query_filter).count() > 0

View File

@ -1,94 +0,0 @@
import base64
from fastapi import APIRouter, Depends, HTTPException
from typing import List
import schemas
from users import User, UserDB, UserTable, current_user
from db import get_db
import models
from sqlalchemy.orm import Session as DbSession, lazyload
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.exc import IntegrityError
from fastapi import status
from sqlalchemy.sql import select
router = APIRouter()
@router.post("/sessions",
response_model=schemas.Session,
tags=["sessions"],
status_code=status.HTTP_201_CREATED)
def create_session(session: schemas.SessionBase,
db: DbSession = Depends(get_db),
user: User = Depends(current_user)):
session_props = session.dict()
session_props['user'] = user.id
session_props['data'] = base64.b64decode(session_props['data'])
db_obj = models.Session(**session_props)
db.add(db_obj)
db.commit()
return db_obj
@router.get("/sessions", response_model=List[schemas.Session], tags=["sessions"])
def list_sessions(skip=0,
limit=100,
db: DbSession = Depends(get_db),
user: User = Depends(current_user)):
return db.query(models.Session).filter(models.Session.user == user.id).order_by(
models.Session.start_time.desc()).offset(skip).limit(limit).all()
@router.post("/friends/request_friendship/{user_id}", tags=["friends"])
def create_friend_request(other_user_id: str,
db: DbSession = Depends(get_db),
user: User = Depends(current_user)):
if models.Friendship.are_friends(db, other_user_id, user.id):
raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE, detail="already friends")
FR = models.FriendRequest
friend_request_from_other_user = db.query(FR).filter(FR.requesting_user == other_user_id,
FR.receiving_user == user.id).count()
if friend_request_from_other_user > 0:
raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE,
detail="Friend request exist from other user, accept it")
else:
try:
new_friend_request = FR(requesting_user=user.id, receiving_user=other_user_id)
db.add(new_friend_request)
db.commit()
return {"msg": "ok"}
except IntegrityError:
raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE,
detail="Friend request already exists")
@router.post("/friends/accept_friendship/{user_id}", tags=["friends"])
def accept_friend_request(other_user_id: str,
db: DbSession = Depends(get_db),
user: User = Depends(current_user)):
FR = models.FriendRequest
try:
friend_request = db.query(FR).filter(FR.requesting_user == other_user_id,
FR.receiving_user == user.id).one()
except NoResultFound:
raise HTTPException(status_code=404, detail="No matching friend request found")
models.Friendship.befriend(db, other_user_id, user.id)
db.delete(friend_request)
db.commit()
return {"msg": "ok"}
@router.get("/friends", tags=["friends"], response_model=schemas.FriendsInfo)
def list_friends_info(db: DbSession = Depends(get_db), user: User = Depends(current_user)):
user_obj = db.query(UserTable).filter(UserTable.id == user.id).one()
return schemas.FriendsInfo(incoming_requests=user_obj.friend_requests_in,
outgoing_requests=user_obj.friend_requests_out)
# todo: remove friend requests
# todo: remove friendship
# todo: search user by email
# todo: add usernames to users
# todo: search by username

View File

@ -1,40 +0,0 @@
from typing import Optional, List
from pydantic import BaseModel, conint, UUID4
from pydantic.networks import EmailStr
class SessionBase(BaseModel):
device_id: int
start_time: conint(gt=1546297200)
data: str
value_right_shift: Optional[conint(ge=0, le=32)]
tare_value: Optional[conint(ge=0)]
kg_factor: Optional[conint(ge=0)]
class Config:
orm_mode = True
class UserInfo(BaseModel):
id: UUID4
email: EmailStr
class Config:
orm_mode = True
class Session(SessionBase):
user: UUID4
class FriendRequestCreate(BaseModel):
other_user_id: int
class FriendsInfo(BaseModel):
incoming_requests: List[UserInfo]
outgoing_requests: List[UserInfo]
class Config:
orm_mode = True

View File

@ -1,92 +0,0 @@
from fastapi_users import FastAPIUsers, models
from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase
from fastapi_users.authentication import JWTAuthentication
from config import JWT_SECRET
from fastapi import Request
from db import database, Base
from sqlalchemy.orm import relationship, backref
from sqlalchemy import Integer, Column
from fastapi_users.models import BaseUser
class User(models.BaseUser):
pass
class UserCreate(models.BaseUserCreate):
pass
class UserUpdate(User, models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass
class UserTable(Base, SQLAlchemyBaseUserTable):
#id = Column(Integer, primary_key=True)
sessions = relationship("Session")
friend_requests_in = relationship(
"UserTable",
secondary="friend_request",
primaryjoin=("UserTable.id == FriendRequest.receiving_user"),
secondaryjoin=("UserTable.id == FriendRequest.requesting_user"),
backref=backref("friend_requests_out"))
friends = relationship('UserTable',
secondary="friendship",
primaryjoin=("UserTable.id == Friendship.user_id"),
secondaryjoin=("UserTable.id == Friendship.friend_id"))
user_db = SQLAlchemyUserDatabase(UserDB, database, UserTable.__table__)
jwt_authentication = JWTAuthentication(secret=JWT_SECRET,
lifetime_seconds=60 * 60 * 8,
tokenUrl="auth/jwt/login")
fastapi_users = FastAPIUsers(
user_db,
[jwt_authentication],
User,
UserCreate,
UserUpdate,
UserDB,
)
current_user = fastapi_users.current_user(active=True, verified=True)
current_superuser = fastapi_users.current_user(active=True, superuser=True, verified=True)
def on_after_register(user: UserDB, request: Request):
print(f"User {user.id} has registered.")
def on_after_forgot_password(user: UserDB, token: str, request: Request):
print(f"User {user.id} has forgot their password. Reset token: {token}")
def after_verification_request(user: UserDB, token: str, request: Request):
print(f"Verification requested for user {user.id}. Verification token: {token}")
def add_user_routers(app):
app.include_router(fastapi_users.get_auth_router(jwt_authentication),
prefix="/auth/jwt",
tags=["auth"])
app.include_router(fastapi_users.get_register_router(on_after_register),
prefix="/auth",
tags=["auth"])
app.include_router(
fastapi_users.get_reset_password_router(JWT_SECRET,
after_forgot_password=on_after_forgot_password),
prefix="/auth",
tags=["auth"],
)
app.include_router(
fastapi_users.get_verify_router(JWT_SECRET,
after_verification_request=after_verification_request),
prefix="/auth",
tags=["auth"],
)
app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"])

View File

@ -1,73 +0,0 @@
import os
from typing import Any, Generator
import pytest
from src.db import Base, get_db
from src.main import app as _app
from fastapi import FastAPI
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# Default to using sqlite in memory for fast tests.
# Can be overridden by environment variable for testing in CI against other
# database engines
SQLALCHEMY_DATABASE_URL = os.getenv('TEST_DATABASE_URL', "sqlite://")
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@pytest.fixture(autouse=True)
def app() -> Generator[FastAPI, Any, None]:
"""
Create a fresh database on each test case.
"""
Base.metadata.create_all(engine) # Create the tables.
yield _app
Base.metadata.drop_all(engine)
@pytest.fixture
def db_session(app: FastAPI) -> Generator[Session, Any, None]:
"""
Creates a fresh sqlalchemy session for each test that operates in a
transaction. The transaction is rolled back at the end of each test ensuring
a clean state.
"""
# connect to the database
connection = engine.connect()
# begin a non-ORM transaction
transaction = connection.begin()
# bind an individual Session to the connection
session = Session(bind=connection)
yield session # use the session in tests.
session.close()
# rollback - everything that happened with the
# Session above (including calls to commit())
# is rolled back.
transaction.rollback()
# return connection to the Engine
connection.close()
@pytest.fixture()
def client(app: FastAPI, db_session: Session) -> Generator[TestClient, Any, None]:
"""
Create a new FastAPI TestClient that uses the `db_session` fixture to override
the `get_db` dependency that is injected into routes.
"""
def _get_test_db():
try:
yield db_session
finally:
pass
app.dependency_overrides[get_db] = _get_test_db
with TestClient(app) as client:
yield client

View File

@ -1,11 +0,0 @@
from backend.src.db import DbSession
from src.schemas import Session
from fastapi import FastAPI
from src.db import DbSession
from fastapi.testclient import TestClient
#----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
def test_session_create(app: FastAPI, db_session: DbSession, client: TestClient):
pass

View File

@ -9,13 +9,12 @@ import tempfile
# --------------------------------------------- Config ----------------------------------------------------------
NODE_PATH = "/home/martin/node-v14.17.0-linux-x64/bin"
JDK8_PATH = "/usr/lib/jvm/java-8-openjdk-amd64/bin/"
NODE_PATH = "/home/martin/node-v18.18.0-linux-x64/bin"
JDK8_PATH = "/usr/lib/jvm/java-11-openjdk-amd64/bin/"
PUBLIC_URL = "https://swimtracker.bauer.tech"
DEPLOY_HOST = "server"
DEPLOY_PATH = "/volumes/swimtracker"
DEPLOY_PATH = "/docker/web/volumes/static-sites/swimtracker"
APP_UPDATE_FOLDER = "app-update"
APP_FOLDER = "app"
@ -91,8 +90,7 @@ def deploy_apk_update_files():
def deploy_apk():
with tempfile.TemporaryDirectory() as tmpdirname:
apk_file = os.path.join(tmpdirname, "app.apk")
cmd = ["turtle", "build:android", "--type", "apk", "--public-url", android_idx_url,
"-o", apk_file]
cmd = ["eas", "build", "--platform", "android", "--local", "--profile", "production", "--output", "app.apk"]
subprocess.check_call(cmd, env=env_with_node_path)
target_file = os.path.join(DEPLOY_PATH, APP_FOLDER, f"swimtracker-{version_name}.apk")
scp.put(apk_file, target_file)

2
doc/devices.md Normal file
View File

@ -0,0 +1,2 @@
swimtracker-4525a0 Garten
swimtracker-452700 Testgerat

68
doc/environment-setup.md Normal file
View File

@ -0,0 +1,68 @@
# Environment Setup
as of 2023-09
1. nodejs
---------
- Downloaded version `node-v18.18.0-linux-x64` and put the `node-v18.18.0-linux-x64/bin` into the path via `~/.config/fish/config.fish`
- Installed yarn as package manager (Source `https://yarnpkg.com/getting-started/install`)
- `corepack enable`
2. expo
-------
(Source `https://reactnative.dev/docs/environment-setup`)
- `yarn create expo-app SwimTracker`
- Starting with `yarn start -w` doesn't work and suggests runnig
`npx expo install react-native-web react-dom @expo/webpack-config`
3. Run
------
npx expo start -w
4. Install Dependencies
-----------------------
use `npx expo install` instead of `yarn add` !
# Redux stuff
- react-redux
- redux-persist
- @react-native-async-storage/async-storage
# Translation
i18n-js
expo-localization
react-native-gesture-handler
react-native-reanimated
expo-app-loading # TODO deprecated
@react-navigation/native
@react-navigation/stack
react-native-safe-area-context # to not cover status bar etc
react-native-screens # not used directly, but somehow required
# Data handling
msgpack-lite
immutable
reconnecting-websocket
# Webdav reponse parsing
react-xml-parser
# Graph
react-native-svg
# Time since
# to be replaced
react-native-swipe-list-view

View File

@ -8,8 +8,23 @@ npm -g install turtle-cli --legacy-peer-deps
expo export --public-url https://swimtracker.bauer.tech/app-update
scp -r dist/* root@server:"/volumes/swimtracker/app-update/"
# Needs jdk 8
apt install openjdk-8-jdk-headless
export PATH=/usr/lib/jvm/java-8-openjdk-amd64/bin/:$PATH
turtle build:android --type apk --public-url https://swimtracker.bauer.tech/app-update/android-index.json -o app.apk
# New
npm install -g eas-cli
# check eas.json from profiles
eas build --platform android --local --profile production
eas build --platform android --local --profile preview --output swimtracker-0.1.apk

View File

@ -1,7 +0,0 @@
lifebuoy
power-plug
ruler
select-arrows

View File

@ -1,11 +0,0 @@
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",
lastSessions: "Letzte Sessions",
mainMenu_social: "Freunde",
mainMenu_swimNow: "Jetzt schwimmen",
}

View File

@ -1,11 +0,0 @@
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",
lastSessions: "Last Sessions",
mainMenu_social: "Social",
mainMenu_swimNow: "Swim now"
}

35493
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,59 +0,0 @@
{
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"eject": "expo eject",
"test": "jest"
},
"jest": {
"preset": "jest-expo",
"transformIgnorePatterns": [
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*)"
]
},
"dependencies": {
"@react-native-async-storage/async-storage": "^1.13.0",
"@react-native-community/masked-view": "0.1.10",
"@react-navigation/native": "^5.4.2",
"@react-navigation/stack": "^5.3.9",
"expo": "^41.0.0",
"expo-app-loading": "^1.0.3",
"expo-blur": "~9.0.3",
"expo-keep-awake": "~9.1.2",
"expo-linear-gradient": "~9.1.0",
"expo-localization": "~10.1.0",
"i18n-js": "^3.8.0",
"immutable": "^4.0.0-rc.12",
"moment": "^2.27.0",
"msgpack-lite": "^0.1.26",
"msgpack5": "^4.2.1",
"prop-types": "^15.7.2",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "0.63.4",
"react-native-chart-kit": "^3.13.0",
"react-native-gesture-handler": "~1.10.2",
"react-native-reanimated": "~2.1.0",
"react-native-safe-area-context": "3.2.0",
"react-native-screens": "~3.0.0",
"react-native-svg": "12.1.0",
"react-native-svg-web": "^1.0.7",
"react-native-swipe-list-view": "^3.2.3",
"react-native-unimodules": "~0.13.3",
"react-native-web": "~0.13.12",
"react-redux": "^7.2.0",
"react-xml-parser": "^1.1.6",
"reconnecting-websocket": "^4.4.0",
"redux": "^4.0.5",
"redux-persist": "^6.0.0"
},
"devDependencies": {
"babel-preset-expo": "8.3.0",
"jest-expo": "^41.0.0",
"react-test-renderer": "^16.13.1"
},
"private": true
}

88
playground/FollowPaths.js Normal file
View File

@ -0,0 +1,88 @@
import React, { Component } from 'react'
import { View, PanResponder, GestureResponderEvent } from 'react-native'
import Svg, {
Circle,
Ellipse,
G,
LinearGradient,
RadialGradient,
Line,
Path,
Polygon,
Polyline,
Rect,
Symbol,
Use,
Defs,
Stop
} from 'react-native-svg';
export default class Foo extends Component {
panResponder = null;
constructor(props) {
super(props)
this.state = { x: 200, y: 200, initX: 0, initY: 0 }
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: () => {
},
onPanResponderStart: (evt, gestureState) => {
console.log("start", gestureState);
this.setState({ initX: this.state.x, initY: this.state.y });
},
onPanResponderMove: (evt, gs) => {
//console.log(gs.dx + ' ' + gs.dy)
const newX = this.state.initX + gs.dx;
const newY = this.state.initY + gs.dy;
this.setState({ x: newX, y: newY });
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gs) => {
console.log('Release ' + gs.dx + ' ' + gs.dy);
//this.setState({ x: this.state.x, y: 0 });
},
onShouldBlockNativeResponder: (evt, gestureState) => {
// Returns whether this component should block native components from becoming
// the JS responder. Returns true by default. Is currently only supported on
// android.
return true;
},
})
}
componentDidMount() {
}
render() {
return (
<Svg height="500" width="500">
<Circle
{...this.panResponder.panHandlers}
x={this.state.x}
y={this.state.y}
cx="50"
cy="50"
r="20"
stroke="blue"
strokeWidth="3.5"
fill="white" />
</Svg>
)
}
}

139
playground/SliderDraft.js Normal file
View File

@ -0,0 +1,139 @@
import React, { useRef } from "react";
import {
StyleSheet,
View,
StatusBar,
ImageBackground,
Text,
TouchableOpacity,
Animated,
PanResponder,
} from "react-native";
import EntypoIcon from "react-native-vector-icons/Entypo";
import Svg, { G, Polyline, Line, Circle, Rect, Text as SvgText } from 'react-native-svg';
let AnimatedCircle = Animated.createAnimatedComponent(Circle);
let AnimatedG = Animated.createAnimatedComponent(G);
function SliderDraftView(props) {
const pan = useRef(new Animated.ValueXY()).current;
/*
const panResponder = useRef(
PanResponder.create({
onPanResponderTerminationRequest: () => { console.log("p1"); return false; },
onStartShouldSetPanResponder: () => { console.log("p2"); return true; },
onMoveShouldSetPanResponder: () => { console.log("p3"); return true; },
//onPanResponderMove: Animated.event([
// null,
// { dx: pan.x, dy: pan.y }
//]),
onPanResponderMove: (e, gesture) => {
console.log(gesture);
},
onPanResponderRelease: () => {
console.log("release");
Animated.spring(pan, { toValue: { x: 0, y: 0 } }).start();
}
})
);//.current;
*/
const panResponder = useRef(PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderMove: (_, { dx, dy }) => {
console.log("bla", dx, dy);
cx.setValue(dx);
cy.setValue(dy);
setCurrentPoint({ x: dx, y: dy });
},
onPanResponderRelease: (e, { dx, dy }) => {
console.log("release", dx, dy);
cx.extractOffset();
cy.extractOffset();
offsetX = offsetX + dx;
offsetY = offsetY + dy;
}
})).current;
console.log({ ...panResponder.panHandlers });
return (
<View style={{ flex: 1 }}>
<StatusBar barStyle="light-content" backgroundColor="rgba(0,0,0,0.4)" translucent={true} />
<ImageBackground
source={require("../assets/pool_sky_background_blurred.jpg")}
resizeMode="cover"
style={{ flex: 1 }}
>
<View style={setupViewStyles.container}>
<View style={{ flexDirection: "row", alignItems: "center" }}>
{props.backButton &&
<TouchableOpacity onPress={() => props.navigation.goBack()}>
<EntypoIcon name="chevron-left" style={setupViewStyles.backButton}></EntypoIcon>
</TouchableOpacity>
}
<Text style={setupViewStyles.headerText}>
Slider Draft
</Text>
</View>
<View style={{ flex: 1, justifyContent: "center" }}>
<Svg height="100%" width="100%" viewBox="0 0 200 200" {...panResponder.panHandler}>
<Rect x="10" y="10" rx="15" width="160" height="40" fill="rgba(255, 255, 255, 0.7)" />
<SvgText x="85" y="34" fill="rgba(80, 80, 80, 1)">42</SvgText>
<AnimatedCircle
x={pan.x} y={pan.y} r="20"
></AnimatedCircle>
<G >
<Line x1="25" y1="20" x2="25" y2="40" stroke="rgba(80, 80, 80, 0.7)" />
<Line x1="45" y1="20" x2="45" y2="40" stroke="rgba(80, 80, 80, 0.7)" />
<Line x1="65" y1="20" x2="65" y2="40" stroke="rgba(80, 80, 80, 0.7)" />
<Line x1="85" y1="20" x2="85" y2="40" stroke="rgba(80, 80, 80, 0.7)" />
<Line x1="105" y1="20" x2="105" y2="40" stroke="rgba(80, 80, 80, 0.7)" />
<Line x1="125" y1="20" x2="125" y2="40" stroke="rgba(80, 80, 80, 0.7)" />
<Line x1="145" y1="20" x2="145" y2="40" stroke="rgba(80, 80, 80, 0.7)" />
</G>
</Svg>
</View>
</View>
</ImageBackground>
</View>
)
}
const setupViewStyles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "space-between",
width: "80%",
marginLeft: 40,
marginTop: 60,
},
headerText: {
color: "rgba(255,255,255,1)",
fontSize: 25,
},
subtext: {
color: "rgba(255,255,255,1)",
textAlign: "left",
fontSize: 18,
lineHeight: 25,
width: "80%",
marginBottom: 50,
},
backButton: {
color: "rgba(255,255,255,1)",
fontSize: 40
},
});
export default SliderDraftView;

View File

@ -0,0 +1,22 @@
# %%
import msgpack
#data = [130, 164, 116, 105, 109, 101, 206, 0, 7, 200, 131, 163, 109, 115, 103, 217, 56, 110, 101, 119, 32, 119, 101, 98, 115, 111, 99, 107, 101, 116, 32, 99, 111, 110, 110, 101, 99, 116, 105, 111, 110, 44, 32, 115, 116, 111, 114, 105, 110, 103, 32, 97, 116, 32, 112, 111, 115, 32, 49, 32, 45, 32, 111, 99, 99, 117, 112, 97, 110, 99, 121, 58]
#data = [130, 164, 116, 105, 109, 101, 206, 0, 4, 35, 94, 163, 109, 115, 103, 217, 55, 110, 101, 119, 32, 119, 101, 98, 115, 111, 99, 107, 101, 116, 32, 99, 111, 110, 110, 101, 99, 116, 105, 111, 110, 44, 32, 115, 116, 111, 114, 105, 110, 103, 32, 97, 116, 32, 112, 111, 115, 32, 49, 32, 45, 32, 111, 99, 99, 117, 112, 97, 110, 99, 121]
data = [130, 164, 116, 105, 109, 101, 205, 14, 216, 163, 109, 115, 103, 217, 37, 83, 112, 105, 102, 102, 115, 32, 115, 105, 122, 101, 58, 32, 49, 50, 32, 77, 66, 44, 32, 115, 101, 116, 117, 112, 32, 116, 105, 109, 101, 32, 49, 32, 115, 101, 99]
byte_data = b''
for e in data:
byte_data += e.to_bytes(1, "big")
#print("length", len(byte_data))
#print(byte_data.decode(errors="ignore"))
#print(msgpack.unpackb(byte_data))
# %%
for i, e in enumerate(data):
print(i, ":", e, bin(e), chr(e))
# %%
#last_msg = data[17:]
#print("len last msg", len(last_msg))
# %%

View File

@ -1,45 +0,0 @@
import moment from 'moment/min/moment-with-locales';
function timeSince(timeStamp, lang = 'de') {
moment.locale(lang);
const now = Math.floor((new Date()).getTime() / 1000);
const secondsPast = now - timeStamp;
if (secondsPast <= 6 * 3600) {
return moment().seconds(-secondsPast).fromNow();
}
else{
const timeStampDate = new Date(timeStamp * 1000);
const dateNow = new Date();
const timeStampMoment = moment.unix(timeStamp);
let dateStr = "";
if (timeStampDate.getDate() == dateNow.getDate())
dateStr = "Heute, " + timeStampMoment.format("HH:mm");
else if (timeStampDate.getDate() + 1 == dateNow.getDate())
dateStr = "Gestern, " + timeStampMoment.format("HH:mm");
else {
dateStr = timeStampMoment.format("ddd, DD.MM.YY um HH:mm");
}
return dateStr;
}
}
const toTimeStr = seconds => {
let minuteStr = String(Math.floor(seconds / 60));
if (minuteStr.length < 2)
minuteStr = "0" + minuteStr;
let secondStr = String(Math.floor(seconds % 60));
if (secondStr.length < 2)
secondStr = "0" + secondStr;
return minuteStr + ":" + secondStr;
}
export { toTimeStr, timeSince };

8657
yarn.lock

File diff suppressed because it is too large Load Diff