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:
parent
00d9c824d0
commit
cac178f06d
|
|
@ -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] = {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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': {
|
||||||
|
|
|
||||||
|
|
@ -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']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue