diff --git a/.babelrc b/.babelrc old mode 100755 new mode 100644 diff --git a/.eslintrc.yaml b/.eslintrc.yaml old mode 100755 new mode 100644 index 6020c9cb..889d79b0 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -14,3 +14,4 @@ globals: window: true Event: true customElements: true + document: true diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 diff --git a/package-lock.json b/package-lock.json index cee6e1cf..22d7d50b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "license": "MIT", "dependencies": { "@kalkih/lz-string": "^1.4.5", + "@mdi/js": "^7.4.47", "custom-card-helpers": "^1.6.6", "d3-interpolate": "^3.0.1", "lit-element": "^2.2.1", @@ -1331,6 +1332,11 @@ "resolved": "https://registry.npmjs.org/@kalkih/lz-string/-/lz-string-1.4.5.tgz", "integrity": "sha512-F5eIc4ER1sYItRMyIjpmvbV9hb25ydPCOB+2YN+jwljugEf0s+jfEqSpvnmrPYvSRogNkJtVHc2g+NucmLvGHg==" }, + "node_modules/@mdi/js": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", + "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==" + }, "node_modules/@nicolo-ribaudo/chokidar-2": { "version": "2.1.8-no-fsevents", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.tgz", @@ -16864,6 +16870,11 @@ "resolved": "https://registry.npmjs.org/@kalkih/lz-string/-/lz-string-1.4.5.tgz", "integrity": "sha512-F5eIc4ER1sYItRMyIjpmvbV9hb25ydPCOB+2YN+jwljugEf0s+jfEqSpvnmrPYvSRogNkJtVHc2g+NucmLvGHg==" }, + "@mdi/js": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", + "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==" + }, "@nicolo-ribaudo/chokidar-2": { "version": "2.1.8-no-fsevents", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.tgz", diff --git a/package.json b/package.json index 4e76eced..2616199a 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "license": "MIT", "dependencies": { "@kalkih/lz-string": "^1.4.5", + "@mdi/js": "^7.4.47", "custom-card-helpers": "^1.6.6", "d3-interpolate": "^3.0.1", "lit-element": "^2.2.1", diff --git a/rollup.config.js b/rollup.config.js index e251f483..ff5dbf1b 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -25,7 +25,7 @@ export default { plugins: [ commonjs(), json({ - include: 'package.json', + include: ['package.json', 'src/localize/languages/*'], preferConst: true, }), resolve(), diff --git a/scripts/update_readme.sh b/scripts/update_readme.sh old mode 100755 new mode 100644 diff --git a/src/editor/components/colorSelector.js b/src/editor/components/colorSelector.js new file mode 100644 index 00000000..1a12835f --- /dev/null +++ b/src/editor/components/colorSelector.js @@ -0,0 +1,130 @@ +import { mdiClose } from '@mdi/js'; +import { fireEvent } from 'custom-card-helpers'; +import { css, html, LitElement } from 'lit-element'; +import { isValidHex, convertColorNameToHex } from '../editorUtils'; + +export const colorSelector = { + hex_color: {}, +}; + +export class CustomColorSelector extends LitElement { + static get properties() { + return { + hass: { attribute: false }, + selector: { attribute: false }, + value: {}, + label: {}, + }; + } + + render() { + const isHex = isValidHex(this.value); + const colorValue = isHex ? this.value : convertColorNameToHex(this.value); + return html` +
+ + ${this.selector.hex_color.clearable ? html` + + ` : html``} +
+ `; + } + + valueChanged(ev) { + const value = (ev.target).value || '#000000'; + fireEvent(this, 'value-changed', { value }); + } + + clearValue() { + fireEvent(this, 'value-changed', { undefined }); + } + + static get styles() { + return css` + #hex { + display: flex; + align-items: center; + margin: 4px 0px; + flex: 1; + } + + .input-wrapper { + width: 48px; + height: 48px; + box-sizing: border-box; + border: 1px solid var(--outline-color); + position: relative; + border-radius: 50%; + } + + #hex:hover .input-wrapper { + border: 2px solid var(--outline-color); + } + + .label { + font-family: var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif)); + color: var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, .87)); + font-size: 1em; + line-height: var(--mdc-typography-body2-line-height, 1.25rem); + font-weight: var(--mdc-typography-body2-font-weight, 400); + flex-grow: 1; + padding-inline-start: 4px; + } + + .overflow { + width: 100%; + height: 100%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + border-radius: inherit; + } + + #hex input { + min-width: 200%; + min-height: 200%; + } + + #hex .empty::before { + background: + repeating-conic-gradient( + var(--secondary-background-color) 0 90deg, + var(--disabled-text-color) 0 180deg) + 0 0/40px 40px round; + content: ''; + min-width: 200%; + min-height: 200%; + display: block; + } + + .color-container { + display: flex; + align-items: center; + } + + .clear-button { + --mdc-icon-size: 20px; + color: var(--input-dropdown-icon-color); + } + `; + } +} + +customElements.define('ha-selector-hex_color', CustomColorSelector); diff --git a/src/editor/components/entitiesEditor.js b/src/editor/components/entitiesEditor.js new file mode 100644 index 00000000..1f65f970 --- /dev/null +++ b/src/editor/components/entitiesEditor.js @@ -0,0 +1,178 @@ +import { mdiPencil, mdiDrag } from '@mdi/js'; +import { fireEvent } from 'custom-card-helpers'; +import { LitElement, html, css } from 'lit-element'; + +const SCHEMA = [ + { + name: 'entity', + selector: { entity: {} }, + }, +]; + +class EntitiesEditor extends LitElement { + constructor() { + super(); + this.newEntity = {}; + } + + static get properties() { + return { + hass: { attribute: false }, + entities: { attribute: false }, + newEntity: {}, + }; + } + + computeLabel(schema) { + return this.hass.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); + } + + computeHelper(schema, data) { + if (schema.selector.entity) { + return data.entity; + } + return ''; + } + + render() { + if (!this.entities) { + return html` +
+ +
+ `; + } + + return html` + +
+ ${this.entities.map((entity, index) => html` +
+
+ +
+ this.computeHelper(schema, typeof entity === 'object' ? entity : { entity })} + @value-changed=${this.valueChanged} + > + +
+ `)} +
+ +
+
+ +
+ `; + } + + rowMoved(ev) { + ev.stopPropagation(); + if (!this.entities || !this.hass) { + return; + } + const { oldIndex, newIndex } = ev.detail; + const newEntities = this.entities.concat(); + + newEntities.splice(newIndex, 0, newEntities.splice(oldIndex, 1)[0]); + + fireEvent(this, 'config-changed', newEntities); + } + + addEntity(ev) { + ev.stopPropagation(); + const value = ev.detail.value || ''; + if (this.entities === undefined) { + fireEvent(this, 'config-changed', [value]); + return; + } + if (value === '' || value.entity === undefined) { + return; + } + fireEvent(this, 'config-changed', [...this.entities, value]); + } + + editRow(ev) { + const index = (ev.currentTarget).index || 0; + fireEvent(this, 'edit-row', index); + } + + valueChanged(ev) { + if (!this.entities || !this.hass) { + return; + } + const value = ev.detail.value.entity || ''; + const index = (ev.target).index || 0; + const newConfigEntities = this.entities.concat(); + + if (value === '' || value === undefined) { + newConfigEntities.splice(index, 1); + } else if (typeof newConfigEntities[index] === 'object') { + newConfigEntities[index] = { + ...newConfigEntities[index], + entity: value || '', + }; + } else { + newConfigEntities[index] = { + entity: value || '', + }; + } + + fireEvent(this, 'config-changed', newConfigEntities); + } + + static get styles() { + return css` + .entity { + display: flex; + align-items: center; + margin-bottom: 8px; + } + + .entity > ha-form { + flex-grow: 1; + } + + .handle { + cursor: grab; + padding-inline-end: 8px; + align-self: flex-start; + margin: 16px 0; + } + + .add-item { + margin-bottom: 24px; + } + + .edit-entity { + align-self: flex-start; + margin: 4px 0px; + } + `; + } +} + +customElements.define('mini-graph-card-entities-editor', EntitiesEditor); diff --git a/src/editor/components/entityEditor.js b/src/editor/components/entityEditor.js new file mode 100644 index 00000000..30498c78 --- /dev/null +++ b/src/editor/components/entityEditor.js @@ -0,0 +1,82 @@ +import { fireEvent } from 'custom-card-helpers'; +import { LitElement, html } from 'lit-element'; +import { ENTITYSCHEMA } from '../editorConst'; +import './colorSelector'; +import './subPageHeader'; + +class EntityEditor extends LitElement { + static get properties() { + return { + hass: { attribute: false }, + config: { attribute: false }, + }; + } + + computeLabel(schema) { + const localized = this.hass.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); + if (localized !== '') { + return localized; + } + return this.hass.localize(`ui.panel.lovelace.editor.card.mgc.${schema.name}`); + } + + localizeValue(key) { + const localized = this.hass.localize(`ui.panel.lovelace.editor.card.mgc.values.${key}`); + if (localized !== '') { + return localized; + } + return key; + } + + computeHelper(schema, data) { + if (schema.name === 'entity') { + return data.entity; + } + return ''; + } + + render() { + if (!this.config || !this.hass) { + return html``; + } + + const isObject = typeof this.config === 'object'; + + const DATA = { + entity: this.config, + }; + + return html` + + this.computeHelper(schema, isObject ? this.config : DATA)} + .localizeValue=${this.localizeValue} + @value-changed=${this.valueChanged} + > + `; + } + + goBack() { + fireEvent(this, 'go-back'); + } + + valueChanged(ev) { + if (!this.config || !this.hass) { + return; + } + const target = ev.target || ''; + + const value = target.checked !== undefined ? target.checked : ev.detail.value; + + fireEvent(this, 'config-changed', value); + } +} + +customElements.define('mini-graph-card-entity-editor', EntityEditor); diff --git a/src/editor/components/mgc_list.js b/src/editor/components/mgc_list.js new file mode 100644 index 00000000..f71252ac --- /dev/null +++ b/src/editor/components/mgc_list.js @@ -0,0 +1,119 @@ +import { mdiPlus, mdiClose } from '@mdi/js'; +import { fireEvent } from 'custom-card-helpers'; +import { LitElement, css, html } from 'lit-element'; + +class MGCList extends LitElement { + static get properties() { + return { + hass: { attribute: false }, + data: { attribute: false }, + schema: { attribute: false }, + }; + } + + computeLabel(schema) { + const localized = this.hass.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); + if (localized !== '') { + return localized; + } + return this.hass.localize(`ui.panel.lovelace.editor.card.mgc.${schema.name}`); + } + + render() { + if (!this.data) { + return html` +
+ +
+
+ `; + } + + return html` +
+ +
+
+ ${this.data.map((row, index) => html` +
+ + +
+ `)} +
+ `; + } + + valueChanged(ev) { + ev.stopPropagation(); + if (!this.data || !this.hass) { + return; + } + const value = ev.detail.value || ''; + const index = (ev.target).index || 0; + const newListValue = this.data.concat(); + + newListValue[index] = value; + + fireEvent(this, 'value-changed', { value: newListValue }); + } + + addRow(ev) { + ev.stopPropagation(); + const value = {}; + if (!this.data) { + fireEvent(this, 'value-changed', { value: [value] }); + return; + } + + fireEvent(this, 'value-changed', { value: [...this.data, value] }); + } + + removeRow(ev) { + ev.stopPropagation(); + if (!this.data || !this.hass) { + return; + } + const index = (ev.currentTarget).index || 0; + const newListValue = this.data.concat(); + + newListValue.splice(index, 1); + + fireEvent(this, 'value-changed', { value: newListValue }); + } + + static get styles() { + return css` + .list-entry { + display: flex; + align-items: center; + margin-bottom: 8px; + } + .list-entry ha-form { + flex-grow: 1; + } + `; + } +} + +customElements.define('ha-form-mgc-list', MGCList); diff --git a/src/editor/components/subPageHeader.js b/src/editor/components/subPageHeader.js new file mode 100644 index 00000000..0acecd5e --- /dev/null +++ b/src/editor/components/subPageHeader.js @@ -0,0 +1,58 @@ +import { fireEvent } from 'custom-card-helpers'; +import { LitElement, html, css } from 'lit-element'; +import { mdiPlus, mdiArrowLeft } from '@mdi/js'; + +class SubPageHeader extends LitElement { + static get properties() { + return { + name: {}, + adding: { reflect: true }, + }; + } + + render() { + return html` +
+
+ + ${this.name} +
+ ${this.adding ? html` + + ` : html``} +
+ `; + } + + goBack() { + fireEvent(this, 'go-back'); + } + + addRow() { + fireEvent(this, 'add-row'); + } + + static get styles() { + return css` + .header { + display: flex; + justify-content: space-between; + align-items: center; + } + + .back-title { + display: flex; + align-items: center; + font-size: 18px; + } + `; + } +} + +customElements.define('mini-graph-card-subpage-header', SubPageHeader); diff --git a/src/editor/editor.js b/src/editor/editor.js new file mode 100644 index 00000000..7ee98524 --- /dev/null +++ b/src/editor/editor.js @@ -0,0 +1,172 @@ +import { fireEvent } from 'custom-card-helpers'; +import { LitElement, html } from 'lit-element'; +import './components/entitiesEditor'; +import './components/entityEditor'; +import './components/mgc_list'; +import { setupTranslations } from '../localize/localize'; +import { MAINSCHEMA, BOOLEANS } from './editorConst'; +import { booleanToString, stringToBoolean } from './editorUtils'; + +class MiniGraphCardEditor extends LitElement { + static get properties() { + return { + hass: {}, + _config: {}, + subElementEditorConfig: {}, + }; + } + + setConfig(config) { + this._config = config; + this._entities = config.entities; + setupTranslations(this.hass); + } + + valueChanged(ev) { + ev.stopPropagation(); + const newConfig = ev.detail.value || ''; + const newShow = {}; + + if (typeof newConfig.show !== 'undefined') { + Object.keys(newConfig.show).forEach((key) => { + if (typeof newConfig.show[key] !== 'undefined') { + newShow[key] = stringToBoolean(newConfig.show[key]); + } + }); + } + + if (!this._config || !this.hass) { + return; + } + + fireEvent(this, 'config-changed', { + config: + { + ...newConfig, + show: Object.keys(newShow).length !== 0 ? newShow : undefined, + }, + }); + } + + buildShowObject(showObject) { + if (typeof showObject === 'undefined') { + return undefined; + } + const show = {}; + Object.keys(showObject).forEach((key) => { + if (!BOOLEANS.includes(key)) { + show[key] = booleanToString(showObject[key]); + } else { + show[key] = showObject[key]; + } + }); + return show; + } + + entitiesChanged(ev) { + ev.stopPropagation(); + if (!this._config || !this.hass) { + return; + } + fireEvent(this, 'config-changed', { config: { ...this._config, entities: ev.detail } }); + } + + computeLabel(schema) { + let localized = this.hass.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); + if (localized !== '') { + return localized; + } + localized = this.hass.localize(`ui.panel.lovelace.editor.card.mgc.${schema.name}`); + if (localized !== '') { + return localized; + } + return schema.name; + } + + localizeValue(key) { + const localized = this.hass.localize(`ui.panel.lovelace.editor.card.mgc.values.${key}`); + if (localized !== '') { + return localized; + } + return key; + } + + render() { + if (!this.hass || !this._config) { + return html``; + } + + if (this.subElementEditorConfig !== undefined) { + return this.renderSubElement(); + } + + const SHOW = this._config.show; + const DATA = { + ...this._config, + show: this.buildShowObject(SHOW), + }; + + return html` +
+ + +
+ `; + } + + goBack() { + this.subElementEditorConfig = undefined; + } + + renderSubElement() { + switch (this.subElementEditorConfig.type) { + case 'entity': + return html` + + `; + default: + return html``; + } + } + + subElementChanged(ev) { + ev.stopPropagation(); + if (!this._config || !this.hass) { + return; + } + const index = this.subElementEditorConfig.index || 0; + if (index !== undefined) { + const configentities = [...this._entities]; + configentities[index] = ev.detail; + fireEvent(this, 'config-changed', { config: { ...this._config, entities: configentities } }); + } + } + + editRow(ev) { + ev.stopPropagation(); + if (!this._config || !this.hass) { + return; + } + const id = ev.detail; + this.subElementEditorConfig = { type: 'entity', index: id }; + } +} + +customElements.define('mini-graph-card-editor', MiniGraphCardEditor); diff --git a/src/editor/editorConst.js b/src/editor/editorConst.js new file mode 100644 index 00000000..582e272d --- /dev/null +++ b/src/editor/editorConst.js @@ -0,0 +1,546 @@ +import { + mdiAlignHorizontalLeft, mdiArrowExpandVertical, mdiEye, + mdiFormatColorFill, + mdiPalette, + mdiStateMachine, +} from '@mdi/js'; + +const MAINSCHEMA = [ + { + name: 'appearance', + type: 'expandable', + iconPath: mdiPalette, + flatten: true, + schema: [ + { + name: '', + type: 'grid', + schema: [ + { + name: 'name', + label: 'Name', + selector: { text: {} }, + }, + { + name: 'icon', + selector: { icon: {} }, + }, + { + name: 'unit', + selector: { text: {} }, + }, + { + name: 'hour24', + selector: { boolean: {} }, + }, + { + name: 'hours_to_show', + selector: { number: { min: 1 } }, + }, + { + name: 'points_per_hour', + selector: { number: { min: 0.1, step: 0.1 } }, + }, + { + name: 'aggregate_func', + selector: { + select: { + options: [ + { label: 'Average', value: 'avg' }, + { label: 'Median', value: 'median' }, + { label: 'Minimum', value: 'min' }, + { label: 'Maximum', value: 'max' }, + { label: 'First', value: 'first' }, + { label: 'Last', value: 'last' }, + { label: 'Sum', value: 'sum' }, + { label: 'Delta', value: 'delta' }, + { label: 'Diff', value: 'diff' }, + ], + mode: 'dropdown', + translation_key: 'aggregate_func', + }, + }, + }, + { + name: 'group_by', + selector: { + select: { + options: [ + { label: 'Interval', value: 'interval' }, + { label: 'Date', value: 'date' }, + { label: 'Hour', value: 'hour' }, + ], + mode: 'dropdown', + translation_key: 'group_by', + }, + }, + }, + { + name: 'value_factor', + selector: { number: {} }, + }, + { + name: 'bar_spacing', + selector: { number: { min: 0.1, step: 0.1 } }, + }, + { + name: 'line_width', + selector: { number: { min: 0.1, step: 0.1 } }, + }, + { + name: 'color_thresholds_transition', + selector: { + select: { + options: [ + { label: 'Smooth', value: 'smooth' }, + { label: 'Hard', value: 'hard' }, + ], + mode: 'dropdown', + translation_key: 'transition', + }, + }, + }, + { + name: 'animate', + selector: { boolean: {} }, + }, + { + name: 'logarithmic', + selector: { boolean: {} }, + }, + ], + }, + { + name: 'bounds', + type: 'expandable', + iconPath: mdiArrowExpandVertical, + flatten: true, + schema: [ + { + name: '', + type: 'grid', + schema: [ + { + name: 'lower_bound', + selector: { text: {} }, + }, + { + name: 'upper_bound', + selector: { text: {} }, + }, + { + name: 'min_bound_range', + selector: { number: { step: 0.1 } }, + }, + ], + }, + { + name: '', + type: 'grid', + schema: [ + { + name: 'lower_bound_secondary', + selector: { text: {} }, + }, + { + name: 'upper_bound_secondary', + selector: { text: {} }, + }, + { + name: 'min_bound_range_secondary', + selector: { number: { step: 0.1 } }, + }, + ], + }, + ], + }, + { + name: 'alignment', + type: 'expandable', + iconPath: mdiAlignHorizontalLeft, + flatten: true, + schema: [ + { + name: '', + type: 'grid', + schema: [ + { + name: 'align_header', + selector: { + select: { + options: [ + { label: 'Default', value: 'default' }, + { label: 'Left', value: 'left' }, + { label: 'Right', value: 'right' }, + { label: 'Center', value: 'center' }, + ], + mode: 'dropdown', + translation_key: 'alignment', + }, + }, + }, + { + name: 'align_icon', + selector: { + select: { + options: [ + { label: 'Left', value: 'left' }, + { label: 'Right', value: 'right' }, + { label: 'State', value: 'state' }, + ], + mode: 'dropdown', + translation_key: 'alignment', + }, + }, + }, + { + name: 'align_state', + selector: { + select: { + options: [ + { label: 'Left', value: 'left' }, + { label: 'Right', value: 'right' }, + { label: 'Center', value: 'center' }, + ], + mode: 'dropdown', + translation_key: 'alignment', + }, + }, + }, + ], + }, + ], + }, + { + name: 'show', + type: 'expandable', + iconPath: mdiEye, + schema: [ + { + name: '', + type: 'grid', + schema: [ + { + name: 'name', + default: true, + selector: { boolean: {} }, + }, + { + name: 'icon', + default: true, + selector: { boolean: {} }, + }, + { + name: 'state', + selector: { + select: { + options: [ + { label: 'Show', value: 'show' }, + { label: 'Hide', value: 'hide' }, + { label: 'Last', value: 'last' }, + ], + mode: 'dropdown', + translation_key: 'state', + }, + }, + }, + { + name: 'graph', + selector: { + select: { + options: [ + { label: 'Line', value: 'line' }, + { label: 'Bar', value: 'bar' }, + { label: 'Hide', value: 'hide' }, + ], + mode: 'dropdown', + translation_key: 'graph', + }, + }, + }, + { + name: 'fill', + selector: { + select: { + options: [ + { label: 'Show', value: 'show' }, + { label: 'Hide', value: 'hide' }, + { label: 'Fade', value: 'fade' }, + ], + mode: 'dropdown', + translation_key: 'fill', + }, + }, + }, + { + name: 'points', + selector: { + select: { + options: [ + { label: 'Show', value: 'show' }, + { label: 'Hide', value: 'hide' }, + { label: 'Hover', value: 'hover' }, + ], + mode: 'dropdown', + translation_key: 'points', + }, + }, + }, + { + name: 'labels', + selector: { + select: { + options: [ + { label: 'Show', value: 'show' }, + { label: 'Hide', value: 'hide' }, + { label: 'Hover', value: 'hover' }, + ], + mode: 'dropdown', + translation_key: 'labels', + }, + }, + }, + { + name: 'labels_secondary', + selector: { + select: { + options: [ + { label: 'Show', value: 'show' }, + { label: 'Hide', value: 'hide' }, + { label: 'Hover', value: 'hover' }, + ], + mode: 'dropdown', + translation_key: 'labels', + }, + }, + }, + { + name: 'legend', + default: true, + selector: { boolean: {} }, + }, + { + name: 'average', + selector: { boolean: {} }, + }, + { + name: 'extrema', + selector: { boolean: {} }, + }, + + { + name: 'name_adaptive_color', + selector: { boolean: {} }, + }, + { + name: 'icon_adaptive_color', + selector: { boolean: {} }, + }, + ], + }, + ], + }, + ], + }, + { + name: 'color_thresholds', + type: 'expandable', + flatten: true, + iconPath: mdiFormatColorFill, + schema: [ + { + name: 'color_thresholds', + type: 'mgc-list', + schema: [ + { + name: '', + type: 'grid', + column_min_width: '100px', + schema: [ + { + name: 'value', + selector: { number: { step: 0.1 } }, + }, + { + name: 'color', + selector: { hex_color: {} }, + }, + ], + }, + ], + }, + ], + }, + { + name: 'state_map', + type: 'expandable', + flatten: true, + iconPath: mdiStateMachine, + schema: [ + { + name: 'state_map', + type: 'mgc-list', + schema: [ + { + name: '', + type: 'grid', + column_min_width: '100px', + schema: [ + { + name: 'value', + selector: { text: {} }, + }, + { + name: 'label', + selector: { text: {} }, + }, + ], + }, + ], + }, + ], + }, + { + name: 'tap_action', + selector: { ui_action: {} }, + }, +]; + +const ENTITYSCHEMA = [ + { + name: '', + type: 'grid', + schema: [ + { + name: 'entity', + selector: { entity: {} }, + }, + { + name: 'attribute', + selector: { attribute: {} }, + context: { filter_entity: 'entity' }, + }, + { + name: 'name', + selector: { text: {} }, + }, + { + name: 'unit', + selector: { text: {} }, + }, + { + name: 'color', + selector: { hex_color: { clearable: true } }, + }, + { + name: 'state_adaptive_color', + selector: { boolean: {} }, + }, + { + name: 'aggregate_func', + selector: { + select: { + options: [ + { label: 'Average', value: 'avg' }, + { label: 'Median', value: 'median' }, + { label: 'Minimum', value: 'min' }, + { label: 'Maximum', value: 'max' }, + { label: 'First', value: 'first' }, + { label: 'Last', value: 'last' }, + { label: 'Sum', value: 'sum' }, + { label: 'Delta', value: 'delta' }, + { label: 'Diff', value: 'diff' }, + ], + mode: 'dropdown', + translation_key: 'aggregate_func', + }, + }, + }, + ], + }, + { + name: 'show', + type: 'expandable', + iconPath: mdiEye, + flatten: true, + schema: [ + { + name: '', + type: 'grid', + schema: [ + { + name: 'show_state', + default: true, + selector: { boolean: {} }, + }, + { + name: 'show_indicator', + selector: { boolean: {} }, + }, + { + name: 'show_graph', + default: true, + selector: { boolean: {} }, + }, + { + name: 'show_line', + default: true, + selector: { boolean: {} }, + }, + { + name: 'show_fill', + default: true, + selector: { boolean: {} }, + }, + { + name: 'show_points', + default: true, + selector: { boolean: {} }, + }, + { + name: 'show_legend', + default: true, + selector: { boolean: {} }, + }, + { + name: 'show_adaptive_color', + selector: { boolean: {} }, + }, + { + name: 'smoothing', + default: true, + selector: { boolean: {} }, + }, + ], + }, + ], + }, + { + name: 'y_axis', + selector: { + select: { + options: [ + { label: 'Primary', value: 'primary' }, + { label: 'Secondary', value: 'secondary' }, + ], + translation_key: 'y_axis', + }, + }, + }, +]; + +const BOOLEANS = [ + 'name', + 'icon', + 'legend', + 'average', + 'extrema', + 'name_adaptive_color', + 'icon_adaptive_color', +]; + +export { + MAINSCHEMA, + ENTITYSCHEMA, + BOOLEANS, +}; diff --git a/src/editor/editorUtils.js b/src/editor/editorUtils.js new file mode 100644 index 00000000..5159c20d --- /dev/null +++ b/src/editor/editorUtils.js @@ -0,0 +1,34 @@ + +const stringToBoolean = (string) => { + if (/^show$/i.test(string)) { + return true; + } else if (/^hide$/i.test(string)) { + return false; + } + return string; +}; + +const booleanToString = (bool) => { + if (typeof bool === 'boolean') { + if (bool) { + return 'show'; + } else { + return 'hide'; + } + } + + return bool; +}; + +const convertColorNameToHex = (color) => { + const canvas = document.createElement('canvas').getContext('2d'); + canvas.fillStyle = color; + return canvas.fillStyle; +}; + +const isValidHex = hex => /^#([0-9A-F]{3}){1,2}$/i.test(hex); + +export { + stringToBoolean, booleanToString, + convertColorNameToHex, isValidHex, +}; diff --git a/src/localize/languages/de.json b/src/localize/languages/de.json new file mode 100644 index 00000000..dd1e0a75 --- /dev/null +++ b/src/localize/languages/de.json @@ -0,0 +1,49 @@ +{ + "edit_entity": "Entity bearbeiten", + "hour24": "Zeit im 24-Stunden-Format anzeigen", + "points_per_hour": "Datenpunkte pro Stunde", + "group_by": "Daten gruppieren", + "aggregate_func": "Aggregatfunktion", + "align_header": "Kopfzeile Ausrichtung", + "align_icon": "Icon Ausreichtung", + "align_state": "Status Ausreichtung", + "graph": "Graph", + "fill": "Füllung", + "points": "Punkte", + "legend": "Legende", + "average": "Durchschnitt", + "extrema": "Extrema", + "labels": "Beschriftungen", + "labels_secondary": "Sekundäre Beschriftungen", + "name_adaptive_color": "Name adaptive Farbe", + "icon_adaptive_color": "Icon adaptive Farbe", + "animate": "Animieren", + "bar_spacing": "Balkenabstand", + "line_width": "Linienbreite", + "color_thresholds_transition": "Farbschwellen Übergang", + "logarithmic": "Logarithmisch", + "lower_bound": "Untergrenze", + "upper_bound": "Obergrenze", + "min_bound_range": "Min Grenzbereich", + "lower_bound_secondary": "Sekundäre Untergrenze", + "upper_bound_secondary": "Sekundäre Obergrenze", + "min_bound_range_secondary": "Min sekundärer Grenzbereich", + "value_factor": "Faktor", + "color_thresholds": "Farbschwellen", + "state_map": "Statuszuordnung", + "color": "Farbe", + "value": "Wert", + "label": "Beschriftung", + "use_color_thresholds": "Farbschwellen verwenden", + "state_adaptive_color": "Status adaptive Farbe", + "show_state": "Status anzeigen", + "show_indicator": "Indicator anzeigen", + "show_graph": "Graph anzeigen", + "show_line": "Linie anzeigen", + "show_fill": "Füllung anzeigen", + "show_points": "Punkte anzeigen", + "show_legend": "Legende anzeigen", + "show_adaptive_color": "Adaptive Farbe verwenden", + "smoothing": "Glätten", + "y_axis": "Achse" +} \ No newline at end of file diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json new file mode 100644 index 00000000..f52946f0 --- /dev/null +++ b/src/localize/languages/en.json @@ -0,0 +1,131 @@ +{ + "edit_entity": "Edit entity", + "hour24": "Display time in 24-hour format", + "points_per_hour": "Data points per hour", + "group_by": "Data grouping", + "aggregate_func": "Aggregate function", + "align_header": "Header alignment", + "align_icon": "Icon alignment", + "align_state": "State alignment", + "graph": "Graph", + "fill": "Fill", + "points": "Points", + "legend": "Legend", + "average": "Average", + "extrema": "Extrema", + "labels": "Labels", + "labels_secondary": "Secondary labels", + "name_adaptive_color": "Name adaptive color", + "icon_adaptive_color": "Icon adaptive color", + "animate": "Animate", + "bar_spacing": "Bar spacing", + "line_width": "Line width", + "color_thresholds_transition": "Color thresholds transition", + "logarithmic": "Logarithmic", + "lower_bound": "Lower bound", + "upper_bound": "Upper bound", + "min_bound_range": "Min bound range", + "lower_bound_secondary": "Secondary lower bound", + "upper_bound_secondary": "Secondary upper bound", + "min_bound_range_secondary": "Min secondary bound range", + "value_factor": "Value factor", + "color_thresholds": "Color thresholds", + "state_map": "State map", + "color": "Color", + "value": "Value", + "label": "Label", + "use_color_thresholds": "Use color thresholds", + "state_adaptive_color": "State adaptive color", + "show_state": "Show state", + "show_indicator": "Show indicator", + "show_graph": "Show graph", + "show_line": "Show line", + "show_fill": "Show fill", + "show_points": "Show points", + "show_legend": "Show legends", + "show_adaptive_color": "Show adaptive color", + "smoothing": "Smoothing", + "y_axis": "Axis", + "appearance": "Appearance", + "bounds": "Bounds", + "alignment": "Alignment", + "show": "Display", + "values": { + "aggregate_func": { + "options": { + "avg": "Average", + "median": "Median", + "min": "Minimum", + "max": "Maximum", + "first": "First", + "last": "Last", + "sum": "Sum", + "delta": "Delta", + "diff": "Diff" + } + }, + "group_by": { + "options": { + "interval": "Interval", + "date": "Date", + "hour": "Hour" + } + }, + "transition": { + "options": { + "smooth": "Smooth", + "hard": "Hard" + } + }, + "alignment": { + "options": { + "default": "Default", + "left": "Left", + "right": "Right", + "center": "Center", + "state": "State" + } + }, + "state": { + "options": { + "show": "Show", + "hide": "Hide", + "last": "Last" + } + }, + "graph": { + "options": { + "line": "Line", + "bar": "Bar", + "hide": "Hide" + } + }, + "fill": { + "options": { + "show": "Show", + "hide": "Hide", + "fade": "Fade" + } + }, + "points": { + "options": { + "show": "Show", + "hide": "Hide", + "hover": "Hover" + } + }, + "labels": { + "options": { + "show": "Show", + "hide": "Hide", + "hover": "Hover" + } + }, + "y_axis": { + "options":{ + "primary": "Primary", + "secondary": "Secondary" + } + } + } +} \ No newline at end of file diff --git a/src/localize/localize.js b/src/localize/localize.js new file mode 100644 index 00000000..39da4168 --- /dev/null +++ b/src/localize/localize.js @@ -0,0 +1,44 @@ +import * as en from './languages/en.json'; +import * as de from './languages/de.json'; + +const languages = { + en, + de, +}; + +const DEFAULT_LANG = 'en'; + +function processTranslations(obj) { + let keys = []; + Object.keys(obj).forEach((key) => { + if (typeof obj[key] === 'object') { + const subKeys = processTranslations(obj[key]); + keys = keys.concat(subKeys.map(subkey => `${key}.${subkey}`)); + } else { + keys.push(key); + } + }); + return keys; +} + +export default function setupTranslations(hass) { + const lang = hass.locale.language || DEFAULT_LANG; + const hassObject = hass; + const resources = hassObject.resources[hass.locale.language]; + const languageObject = languages[lang] || languages[DEFAULT_LANG]; + + const keys = processTranslations(languageObject); + + keys.forEach((key) => { + if (!key.startsWith('default')) { + const nestedKeys = key.split('.'); + const value = nestedKeys.reduce((a, c) => a[c], languageObject); + + if (value) { + resources[`ui.panel.lovelace.editor.card.mgc.${key}`] = value; + } + } + }); +} + +export { setupTranslations }; diff --git a/src/main.js b/src/main.js index 46496c4a..1d4abb0b 100644 --- a/src/main.js +++ b/src/main.js @@ -7,6 +7,7 @@ import Graph from './graph'; import style from './style'; import handleClick from './handleClick'; import buildConfig from './buildConfig'; +import './editor/editor'; import './initialize'; import { version } from '../package.json'; @@ -50,6 +51,10 @@ class MiniGraphCard extends LitElement { this._md5Config = undefined; } + static getConfigElement() { + return document.createElement('mini-graph-card-editor'); + } + static get styles() { return style; }