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); background: var(--theme-widget-color);
text-align: center; text-align: center;
position: relative; position: relative;
@ -63,6 +63,32 @@ export class PerformanceDashboard extends AppElement {
filter: brightness(150%); 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 }) @state({ type: Object })
dialog dialog
@ -73,31 +99,19 @@ export class PerformanceDashboard extends AppElement {
appState = APP_STATE appState = APP_STATE
render () { 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` return html`
<div class="settings" @click=${this.openSettings}> <div class="settings" @click=${this.openSettings}>
${icon_settings} ${icon_settings}
${this.dialog ? this.dialog : ''} ${this.dialog ? this.dialog : ''}
</div> </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> ${metricConfig}
<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>
` `
} }
@ -124,10 +138,10 @@ export class PerformanceDashboard extends AppElement {
const formattedMetrics = {} const formattedMetrics = {}
for (const [key, value] of Object.entries(metrics)) { for (const [key, value] of Object.entries(metrics)) {
const valueFormatted = fieldFormatter[key] ? fieldFormatter[key](value) : value const valueFormatted = fieldFormatter[key] ? fieldFormatter[key](value) : value
if (valueFormatted.value !== undefined && valueFormatted.unit !== undefined) { if (valueFormatted?.value !== undefined && valueFormatted?.unit !== undefined) {
formattedMetrics[key] = { formattedMetrics[key] = {
value: valueFormatted.value, value: valueFormatted?.value,
unit: valueFormatted.unit unit: valueFormatted?.unit
} }
} else { } else {
formattedMetrics[key] = { 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 // 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: // this is how we implement changes to the global state:
// once any child component sends this CustomEvent we update the global state according // once any child component sends this CustomEvent we update the global state according
// to the changes that were passed to us // to the changes that were passed to us
@ -39,13 +44,22 @@ export class App extends LitElement {
this.addEventListener('triggerAction', (event) => { this.addEventListener('triggerAction', (event) => {
this.app.handleAction(event.detail) 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 // 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? // 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 } // i.e. do something like this.appState = { ..this.appState, ...newState }
updateState = (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 // 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 let initialWebsocketOpenend = true
function initWebsocket () { function initWebsocket () {
// use the native websocket implementation of browser to communicate with backend // 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) => { socket.addEventListener('open', (event) => {
console.log('websocket opened') console.log('websocket opened')
@ -71,7 +71,7 @@ export function createApp (app) {
const data = message.data const data = message.data
switch (message.type) { switch (message.type) {
case 'config': { case 'config': {
app.updateState({ ...app.getState(), config: data }) app.updateState({ ...app.getState(), config: { ...app.getState().config, ...data } })
break break
} }
case 'metrics': { case 'metrics': {

View File

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