refactor Local Storage and its values

- LocalStorageItem class will take care of operations on Local Storage,
  on key, value base

- the functions that work on values like ftp, weight, measurement, page, etc
  will be moved to a corresponding value class to facilitate code reuse.

- fixis Issue #33 `Rider weight value in UI goes to default on page refresh`
This commit is contained in:
dvmarinoff
2021-03-19 19:22:25 +02:00
parent 61d7b281e1
commit 343fc1430c
12 changed files with 222 additions and 150 deletions

2
.gitattributes vendored
View File

@@ -1,3 +1 @@
index.js merge=ours
manifest.webmanifest merge=ours

51
db.js
View File

@@ -4,10 +4,11 @@ import { FileHandler } from './file.js';
import { IDB } from './storage/idb.js';
import { Session } from './storage/session.js';
import { Workout } from './storage/workout.js';
import { values } from './values.js';
import { avgOfArray, maxOfArray, sum,
first, last, round, mps, kph,
timeDiff, fixInRange } from './functions.js';
timeDiff, fixInRange, memberOf } from './functions.js';
let db = {
pwr: 0, // controllable
@@ -49,11 +50,11 @@ let db = {
timestamp: Date.now(),
inProgress: false,
ftp: 0,
weight: 0,
measurement: 'metric',
theme: 'dark',
page: 'home',
ftp: values.ftp.defaultValue(),
weight: values.weight.defaultValue(),
theme: values.theme.defaultValue(),
measurement: values.measurement.defaultValue(),
page: values.page.defaultValue(),
workout: [],
workoutFile: '',
@@ -73,6 +74,7 @@ let db = {
xf.initDB(db);
xf.reg('ui:ant:device:selected', (x, db) => {
db.antDeviceId = db.antSearchList.filter(d => {
return d.deviceNumber === parseInt(x);
@@ -94,25 +96,25 @@ xf.reg('device:cad', (x, db) => db.cad = x);
xf.reg('device:dist', (x, db) => db.distance = x);
xf.reg('pm:power', (x, db) => db.power = x);
xf.reg('ant:hr', (x, db) => db.hrAnt = x);
xf.reg('ant:fec:power', (x, db)=> db.pwr = x);
xf.reg('ant:fec:speed', (x, db)=> db.spd = x);
xf.reg('ant:fec:cadence', (x, db)=> db.cadence = x);
xf.reg('ant:fec:power', (x, db) => db.pwr = x);
xf.reg('ant:fec:speed', (x, db) => db.spd = x);
xf.reg('ant:fec:cadence', (x, db) => db.cadence = x);
xf.reg('ui:page', (x, db) => db.page = x);
xf.reg('ui:ftp', (x, db) => db.ftp = x);
xf.reg('ui:weight', (x, db) => db.weight = x);
xf.reg('ui:theme', (x, db) => {
if(db.theme === 'dark') { db.theme = 'white'; return; }
if(db.theme === 'white') { db.theme = 'dark'; return; }
xf.reg('ui:theme', (_, db) => {
db.theme = values.theme.switch(db.theme);
});
xf.reg('ui:measurement', (x, db) => {
if(db.measurement === 'metric') { db.measurement = 'imperial'; return; }
if(db.measurement === 'imperial') { db.measurement = 'metric'; return; }
xf.reg('ui:measurement', (_, db) => {
db.measurement = values.measurement.switch(db.measurement);
});
xf.reg('storage:ftp', (x, db) => db.ftp = x);
xf.reg('storage:weight', (x, db) => db.weight = x);
xf.reg('storage:theme', (x, db) => db.theme = x);
xf.reg('storage:measurement', (x, db) => db.measurement = x);
xf.reg('storage:set:ftp', (x, db) => {
db.ftp = x;
});
xf.reg('storage:set:weight', (x, db) => db.weight = x);
xf.reg('storage:set:theme', (x, db) => db.theme = x);
xf.reg('storage:set:measurement', (x, db) => db.measurement = x);
xf.reg('ui:workoutFile', (x, db) => {
db.workoutFile = x;
@@ -121,7 +123,6 @@ xf.reg('ui:workoutFile', (x, db) => {
});
xf.reg('ui:workout:set', (x, db) => {
db.workout = db.workouts[x];
// console.log(db.workout.intervals);
});
xf.reg('workout:add', (x, db) => db.workouts.push(x));
@@ -260,9 +261,11 @@ function dbToSession(db) {
workout: db.workout,
records: db.records,
theme: db.theme,
page: db.page,
measurement: db.measurement,
// theme: db.theme,
// weight: db.weight,
// measurement: db.measurement,
};
return session;
}
@@ -271,7 +274,7 @@ xf.reg('app:start', async function (x, db) {
await idb.open('store', 1, 'session');
session = new Session({idb: idb, name: 'session'});
await session.restore();
xf.dispatch('db:ready');
});
xf.reg('lock:beforeunload', (e, db) => {
@@ -306,4 +309,6 @@ xf.reg('file:download:activity', (e, db) => {
});
// Session end
// values.theme.init();
export { db };

View File

@@ -29,6 +29,10 @@ let first = xs => xs[0];
let second = xs => xs[1];
let third = xs => xs[2];
function memberOf(xs, y) {
return xs.filter(x => x === y).length > 0;
}
function isArray(x) {
return Array.isArray(x);
}
@@ -310,6 +314,7 @@ export {
maxOfArray,
sum,
splitAt,
memberOf,
delay,
parseNumber,
toDecimalPoint,

View File

@@ -1,5 +1,3 @@
import 'regenerator-runtime/runtime';
import { xf } from './xf.js';
import { db } from './db.js';
import { Controllable } from './ble/controllable.js';
@@ -13,7 +11,6 @@ import { DeviceController,
FileController,
WorkoutController } from './controllers.js';
import { FileHandler } from './file.js';
import { LocalStorage } from './storage/local-storage.js';
import { Vibrate } from './vibrate.js';
import { DataMock } from './test/mock.js';
@@ -54,8 +51,6 @@ async function start() {
antFec: antFec,
});
let localStorage = new LocalStorage();
xf.dispatch('app:start');
Vibrate({turnOn: true});

10
package-lock.json generated
View File

@@ -1,13 +1,5 @@
{
"name": "flux",
"version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
}
}
"lockfileVersion": 1
}

View File

@@ -9,7 +9,5 @@
},
"author": "",
"license": "ISC",
"dependencies": {
"regenerator-runtime": "^0.13.7"
}
"dependencies": {}
}

View File

@@ -44,7 +44,7 @@ class IDB {
};
});
}
delete(idb, name) {
deleteStore(idb, name) {
const self = this;
let deleteReq = idb.deleteDatabase(name);
@@ -56,11 +56,6 @@ class IDB {
return {};
});
}
update(idb) {
const self = this;
xf.dispatch('idb:update-success', idb);
return idb;
}
createStore(idb, name) {
const self = this;
if (!idb.objectStoreNames.contains(name)) {
@@ -70,6 +65,11 @@ class IDB {
console.error(`idb trying to create store with existing name: ${name}`);
}
}
update(idb) {
const self = this;
xf.dispatch('idb:update-success', idb);
return idb;
}
add(idb, storeName, item) {
const self = this;
return self.transaction(idb, storeName, 'add', item, 'readwrite');
@@ -86,7 +86,7 @@ class IDB {
const self = this;
return self.transaction(idb, storeName, 'getAll', undefined, 'readonly');
}
deleteEntry(idb, storeName, id) {
delete(idb, storeName, id) {
const self = this;
return self.transaction(idb, storeName, 'delete', id, 'readwrite');
}

View File

@@ -1,81 +1,51 @@
import { xf } from '../xf.js';
import { memberOf } from '../functions.js';
class LocalStorage {
constructor(){
class LocalStorageItem {
constructor(args) {
this.key = args.key;
this.default = args.default;
this.validate = args.validate;
this.init();
}
init() {
let self = this;
let ftp = window.localStorage.getItem('ftp');
let weight = window.localStorage.getItem('rkg');
let theme = window.localStorage.getItem('theme');
let measurement = window.localStorage.getItem('measurement');
if(ftp === null || ftp === undefined) {
ftp = 250;
self.setFtp(ftp);
}
if(weight === null || weight === undefined) {
weight = 75;
self.setWeight(weight);
}
if(measurement === null || measurement === undefined) {
measurement = 'metric';
self.setMeasurement(measurement);
}
if(theme === null || theme === undefined) {
theme = 'dark';
self.setTheme(theme);
}
xf.dispatch('storage:ftp', parseInt(ftp));
xf.dispatch('storage:weight', parseInt(weight));
xf.dispatch('storage:theme', theme);
xf.dispatch('storage:measurement', measurement);
xf.sub('ui:ftp', ftp => {
self.setFtp(parseInt(ftp));
});
xf.sub('ui:weight', weight => {
self.setWeight(parseInt(weight));
});
xf.sub('db:theme', theme => {
self.setTheme(theme);
});
xf.sub('db:measurement', measurement => {
self.setMeasurement(measurement);
});
const self = this;
self.restore();
}
setFtp(ftp) {
if(isNaN(ftp) || ftp > 600 || ftp < 30) {
console.warn(`Trying to enter Invalid FTP value in Storage: ${ftp}`);
restore() {
const self = this;
const initialValue = self.get();
if(initialValue === null) {
self.set(self.default);
}
const key = self.key;
const value = self.get();
xf.dispatch(`storage:set:${key}`, value);
}
get() {
const self = this;
const key = self.key;
const value = window.localStorage.getItem(`${key}`);
if(value === undefined || value === null) {
console.warn(`Trying to get non-existing value from Local Storage at key ${key}!`);
}
return value;
}
set(value) {
const self = this;
const key = self.key;
const isValid = self.validate(value);
if(isValid) {
window.localStorage.setItem(`${key}`, value);
} else {
window.localStorage.setItem('ftp', Math.round(ftp));
xf.dispatch('storage:ftp', ftp);
console.warn(`Trying to enter invalid ${key} value in Local Storage: ${value}`);
window.localStorage.setItem(`${key}`, self.default);
}
}
setWeight(weight) {
if(isNaN(weight) || weight > 400 || weight < 20) {
console.warn(`Trying to enter Invalid FTP value in Storage: ${weight}`);
} else {
window.localStorage.setItem('rkg', Math.round(weight));
xf.dispatch('storage:weight', weight);
}
}
setTheme(theme) {
if(theme === 'dark' || theme === 'white') {
window.localStorage.setItem('theme', theme);
} else {
console.warn(`Trying to enter Invalid Theme system value in Storage: ${theme}`);
}
}
setMeasurement(measurement) {
if(measurement === 'metric' || measurement === 'imperial') {
window.localStorage.setItem('measurement', measurement);
} else {
console.warn(`Trying to enter Invalid Measurement system value in Storage: ${measurement}`);
}
delete() {
const self = this;
window.localStorage.removeItem(`${self.key}`);
}
}
export { LocalStorage };
export { LocalStorageItem };

View File

@@ -6,4 +6,4 @@ class Workout extends IDBStore {
postInit() {}
}
export { };
export { Workout };

112
values.js Normal file
View File

@@ -0,0 +1,112 @@
import { xf } from './xf.js';
import { memberOf } from './functions.js';
import { LocalStorageItem } from './storage/local-storage.js';
class Value {
constructor(args) {
this.name = args.name || undefined;
this.init();
this.postInit();
}
init() {
const self = this;
xf.sub('app:start', _ => { self.localStorage(); });
}
postInit() { return; }
localStorage() {
const self = this;
self.local = new LocalStorageItem({key: self.name, default: self.defaultValue(), validate: self.isValid.bind(self)});
xf.sub(`db:${self.name}`, value => {
self.local.set(value);
});
}
defaultValue() { return undefined; }
isValid(value) { return false; }
}
class Ftp extends Value {
defaultValue() { return 200; }
isValid(value) {
value = parseInt(value);
if(isNaN(value) || value > 600 || value < 30) { return false; }
return true;
}
}
class Weight extends Value {
defaultValue() { return 75; }
isValid(value) {
value = parseInt(value);
if(isNaN(value) || value > 400 || value < 20) { return false; }
return true;
}
}
class Theme extends Value {
defaultValue() { return 'dark'; }
collection() { return ['dark', 'white']; }
isValid(value) {
const self = this;
value = String(value);
if(memberOf(self.collection(), value)) { return true; }
return false;
}
switch(value) {
const self = this;
if(value === 'dark') { return 'white'; }
if(value === 'white') { return 'dark'; }
return self.defaultValue();
}
}
class Measurement extends Value {
defaultValue() { return 'metric'; }
collection() { return ['metric', 'imperial']; }
isValid(value) {
const self = this;
value = String(value);
if(memberOf(self.collection(), value)) { return true; }
return false;
}
switch(value) {
const self = this;
if(value === 'metric') { return 'imperial';}
if(value === 'imperial') { return 'metric'; }
return self.defaultValue();
}
}
class Page extends Value {
defaultValue() { return 'home'; }
collection() { return ['settings', 'home', 'workouts']; }
isValid(value) {
const self = this;
value = String(value);
if(memberOf(self.collection(), value)) { return true; }
return false;
}
switch(value) {
const self = this;
return self.defaultValue();
}
}
const ftp = new Ftp({name: 'ftp'});
const weight = new Weight({name: 'weight'});
const theme = new Theme({name: 'theme'});
const measurement = new Measurement({name: 'measurement'});
const page = new Page({name: 'page'});
const values = {
ftp: ftp,
weight: weight,
theme: theme,
measurement: measurement,
page: page
};
export { values };

View File

@@ -12,9 +12,10 @@ import { avgOfArray,
lbsToKg,
kmhToMph,
fixInRange,
fisrt,
first,
last,
} from './functions.js';
import { values } from './values.js';
function ConnectionView(args) {
let dom = args.dom;
@@ -521,15 +522,18 @@ function SettingsView(args) {
measurementBtn: q.get('#measurement-btn'),
};
let ftp = 100;
let weight = 75;
let measurement = 'metric';
let ftp = values.ftp.defaultValue();
let weight = values.weight.defaultValue();
let theme = values.theme.defaultValue();
let measurement = values.measurement.defaultValue();
xf.sub('db:ftp', ftp => {
xf.sub('db:ftp', value => {
ftp = value;
dom.ftp.value = ftp;
});
xf.sub('db:weight', weight => {
xf.sub('db:weight', value => {
weight = value;
dom.weight.value = formatWeight(weight, measurement);
});
@@ -545,6 +549,7 @@ function SettingsView(args) {
dom.theme.classList.remove(`dark-theme`);
dom.theme.classList.remove(`white-theme`);
dom.theme.classList.add(`${name}-theme`);
dom.themeBtn.textContent = `${values.theme.switch(name)}`;
});
xf.sub('change', e => {
@@ -559,10 +564,24 @@ function SettingsView(args) {
xf.dispatch('ui:ftp', ftp);
}, dom.ftpBtn);
xf.sub('keyup', e => {
if(e.keyCode === 13) {
e.preventDefault();
xf.dispatch('ui:ftp', ftp);
}
}, dom.ftp);
xf.sub('pointerup', e => {
xf.dispatch('ui:weight', weight);
}, dom.weightBtn);
xf.sub('keyup', e => {
if(e.keyCode === 13) {
e.preventDefault();
xf.dispatch('ui:weight', weight);
}
}, dom.weight);
xf.sub('pointerup', e => {
xf.dispatch('ui:theme');
}, dom.themeBtn);
@@ -915,7 +934,7 @@ function WorkoutsView() {
descriptions: [],
};
let id = 0;
let id = 0;
const off = `
<svg class="radio radio-off" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
@@ -1104,8 +1123,8 @@ function Views() {
NavigationWidget();
ActivityView();
SettingsView();
WorkoutsView();
SettingsView();
UploadWorkoutView();

View File

@@ -1,27 +1,5 @@
let workouts =
[
// { name: 'Software Test',
// type: 'Debug',
// description: 'Software Test',
// duration: 60,
// xml:
// `<workout_file>
// <author>Marinov</author>
// <name>Software Test</name>
// <description>Software Test</description>
// <sporttype>bike</sporttype>
// <tags></tags>
// <workout>
// <SteadyState Duration="60" Power="0.50"/>
// <SteadyState Duration="60" Power="200"/>
// <SteadyState Duration="60" Power="0.60" SlopeTarget="3" />
// <SteadyState Duration="60" Power="1.21" SlopeTarget="9.5" />
// <IntervalsT Repeat="2" OnDuration="60" OffDuration="30" OnPower="1.06" OffPower="0.95" OnSlopeTarget="6.5" OffSlopeTarget="6" />
// <SteadyState Duration="60" Power="0.50"/>
// </workout>
// </workout_file>`
// },
{ name: 'Dijon',
type: 'VO2 Max',
description: '60/60s or 60 sec ON at 121% of FTP followed by 60 sec OFF. In 2 groups by 8 reps each.',