Add settable metric tiles and settings persistence

Make the metric tiles settable via the settings dialog and implement
persistence of these settings to the browser localStorage (partially
fix #131)
This commit is contained in:
Abász 2023-03-22 13:44:34 +01:00
parent 00d9c824d0
commit cac178f06d
4 changed files with 59 additions and 28 deletions

View File

@ -34,7 +34,7 @@ export class PerformanceDashboard extends AppElement {
}
}
dashboard-metric, dashboard-actions,dashboard-force-curve {
dashboard-metric, dashboard-actions, dashboard-force-curve {
background: var(--theme-widget-color);
text-align: center;
position: relative;
@ -63,6 +63,32 @@ export class PerformanceDashboard extends AppElement {
filter: brightness(150%);
}
`
dashboardMetricComponents = (formattedMetrics, appState) => ({
distance: html`<dashboard-metric .icon=${icon_route} .unit=${formattedMetrics?.totalLinearDistanceFormatted?.unit || 'm'} .value=${formattedMetrics?.totalLinearDistanceFormatted?.value}></dashboard-metric>`,
pace: html`<dashboard-metric .icon=${icon_stopwatch} unit="/500m" .value=${formattedMetrics?.cyclePaceFormatted?.value}></dashboard-metric>`,
power: html`<dashboard-metric .icon=${icon_bolt} unit="watt" .value=${formattedMetrics?.cyclePower?.value}></dashboard-metric>`,
stkRate: html`<dashboard-metric .icon=${icon_paddle} unit="/min" .value=${formattedMetrics?.cycleStrokeRate?.value}></dashboard-metric>`,
heartRate: html`<dashboard-metric .icon=${icon_heartbeat} unit="bpm" .value=${formattedMetrics?.heartrate?.value}>
${formattedMetrics?.heartrateBatteryLevel?.value
? html`<battery-icon .batteryLevel=${formattedMetrics?.heartrateBatteryLevel?.value}></battery-icon>`
: ''}
</dashboard-metric>`,
totalStk: html`<dashboard-metric .icon=${icon_paddle} unit="total" .value=${formattedMetrics?.totalNumberOfStrokes?.value}></dashboard-metric>`,
calories: html`<dashboard-metric .icon=${icon_fire} unit="kcal" .value=${formattedMetrics?.totalCalories?.value}></dashboard-metric>`,
timer: html`<dashboard-metric .icon=${icon_clock} .value=${formattedMetrics?.totalMovingTimeFormatted?.value}></dashboard-metric>`,
forceCurve: html`<dashboard-force-curve .value=${appState?.metrics.driveHandleForceCurve} style="grid-column: span 2"></dashboard-force-curve>`,
actions: html`<dashboard-actions .appState=${appState}></dashboard-actions>`
})
@state({ type: Object })
dialog
@ -73,31 +99,19 @@ export class PerformanceDashboard extends AppElement {
appState = APP_STATE
render () {
const metrics = this.calculateFormattedMetrics(this.appState.metrics)
const metricConfig = [...new Set(this.appState.config.guiConfigs.dashboardMetrics)].reduce((prev, metricName) => {
prev.push(this.dashboardMetricComponents(this.metrics, this.appState)[metricName])
return prev
}, [])
this.metrics = this.calculateFormattedMetrics(this.appState.metrics)
return html`
<div class="settings" @click=${this.openSettings}>
${icon_settings}
${this.dialog ? this.dialog : ''}
</div>
<dashboard-metric .icon=${icon_route} .unit=${metrics?.totalLinearDistanceFormatted?.unit || 'm'} .value=${metrics?.totalLinearDistanceFormatted?.value}></dashboard-metric>
<dashboard-metric .icon=${icon_stopwatch} unit="/500m" .value=${metrics?.cyclePaceFormatted?.value}></dashboard-metric>
<dashboard-metric .icon=${icon_bolt} unit="watt" .value=${metrics?.cyclePower?.value}></dashboard-metric>
<dashboard-metric .icon=${icon_paddle} unit="/min" .value=${metrics?.cycleStrokeRate?.value}></dashboard-metric>
${metrics?.heartrate?.value
? html`
<dashboard-metric .icon=${icon_heartbeat} unit="bpm" .value=${metrics?.heartrate?.value}>
${metrics?.heartrateBatteryLevel?.value
? html`
<battery-icon .batteryLevel=${metrics?.heartrateBatteryLevel?.value}></battery-icon>
`
: ''
}
</dashboard-metric>`
: html`<dashboard-metric .icon=${icon_paddle} unit="total" .value=${metrics?.totalNumberOfStrokes?.value}></dashboard-metric>`}
<dashboard-force-curve .value=${this.appState?.metrics?.driveHandleForceCurve} style="grid-column: span 2"></dashboard-force-curve>
<dashboard-metric .icon=${icon_fire} unit="kcal" .value=${metrics?.totalCalories?.value}></dashboard-metric>
<dashboard-metric .icon=${icon_clock} .value=${metrics?.totalMovingTimeFormatted?.value}></dashboard-metric>
<dashboard-actions .appState=${this.appState}></dashboard-actions>
${metricConfig}
`
}
@ -124,10 +138,10 @@ export class PerformanceDashboard extends AppElement {
const formattedMetrics = {}
for (const [key, value] of Object.entries(metrics)) {
const valueFormatted = fieldFormatter[key] ? fieldFormatter[key](value) : value
if (valueFormatted.value !== undefined && valueFormatted.unit !== undefined) {
if (valueFormatted?.value !== undefined && valueFormatted?.unit !== undefined) {
formattedMetrics[key] = {
value: valueFormatted.value,
unit: valueFormatted.unit
value: valueFormatted?.value,
unit: valueFormatted?.unit
}
} else {
formattedMetrics[key] = {

View File

@ -28,6 +28,11 @@ export class App extends LitElement {
// todo: we also want a mechanism here to get notified of state changes
})
const config = this.appState.config.guiConfigs
Object.keys(config).forEach(key => {
config[key] = JSON.parse(localStorage.getItem(key)) ?? config[key]
})
// this is how we implement changes to the global state:
// once any child component sends this CustomEvent we update the global state according
// to the changes that were passed to us
@ -39,13 +44,22 @@ export class App extends LitElement {
this.addEventListener('triggerAction', (event) => {
this.app.handleAction(event.detail)
})
// notify the app about the triggered action
this.addEventListener('changeGuiSetting', (event) => {
Object.keys(event.detail.config.guiConfigs).forEach(key => {
localStorage.setItem(key, JSON.stringify(event.detail.config.guiConfigs[key]))
})
this.updateState(event.detail)
})
}
// the global state is updated by replacing the appState with a copy of the new state
// todo: maybe it is more convenient to just pass the state elements that should be changed?
// i.e. do something like this.appState = { ..this.appState, ...newState }
updateState = (newState) => {
this.appState = { ...newState }
this.appState = { ...this.appState, ...newState }
}
// return a deep copy of the state to other components to minimize risk of side effects

View File

@ -38,7 +38,7 @@ export function createApp (app) {
let initialWebsocketOpenend = true
function initWebsocket () {
// use the native websocket implementation of browser to communicate with backend
socket = new WebSocket(`ws://${location.host}/websocket`)
socket = new WebSocket('ws://localhost:100/websocket')
socket.addEventListener('open', (event) => {
console.log('websocket opened')
@ -71,7 +71,7 @@ export function createApp (app) {
const data = message.data
switch (message.type) {
case 'config': {
app.updateState({ ...app.getState(), config: data })
app.updateState({ ...app.getState(), config: { ...app.getState().config, ...data } })
break
}
case 'metrics': {

View File

@ -20,6 +20,9 @@ export const APP_STATE = {
// true if upload to strava is enabled
stravaUploadEnabled: false,
// true if remote device shutdown is enabled
shutdownEnabled: false
shutdownEnabled: false,
guiConfigs: {
dashboardMetrics: ['distance', 'timer', 'pace', 'power', 'stkRate', 'totalStk', 'calories', 'actions']
}
}
}