mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 03:06:41 +00:00
Image aspect ratio (#1665)
* Allow user to specify an aspect ratio for various images * added a comment on what is supported * fixed typo * Fixed lint and test errors
This commit is contained in:
parent
a32809e14b
commit
ce3b53a920
3
.gitignore
vendored
3
.gitignore
vendored
@ -21,6 +21,9 @@ lib
|
|||||||
bin
|
bin
|
||||||
dist
|
dist
|
||||||
|
|
||||||
|
# vscode
|
||||||
|
.vscode
|
||||||
|
|
||||||
# Secrets
|
# Secrets
|
||||||
.lokalise_token
|
.lokalise_token
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
24
src/common/util/parse-aspect-ratio.js
Normal file
24
src/common/util/parse-aspect-ratio.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export default function parseAspectRatio(input) {
|
||||||
|
// Handle 16x9, 16:9, 1.78x1, 1.78:1, 1.78
|
||||||
|
// Ignore everything else
|
||||||
|
function parseOrThrow(number) {
|
||||||
|
const parsed = parseFloat(number);
|
||||||
|
if (isNaN(parsed)) throw new Error(`${number} is not a number`);
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (input) {
|
||||||
|
const arr = input.replace(':', 'x').split('x');
|
||||||
|
if (arr.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr.length === 1 ?
|
||||||
|
{ w: parseOrThrow(arr[0]), h: 1 } :
|
||||||
|
{ w: parseOrThrow(arr[0]), h: parseOrThrow(arr[1]) };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore the error
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
@ -58,6 +58,7 @@ class HuiPictureEntityCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|||||||
state-image="[[_config.state_image]]"
|
state-image="[[_config.state_image]]"
|
||||||
camera-image="[[_getCameraImage(_config)]]"
|
camera-image="[[_getCameraImage(_config)]]"
|
||||||
entity="[[_config.entity]]"
|
entity="[[_config.entity]]"
|
||||||
|
aspect-ratio="[[_config.aspect_ratio]]"
|
||||||
></hui-image>
|
></hui-image>
|
||||||
<template is="dom-if" if="[[_showNameAndState(_config)]]">
|
<template is="dom-if" if="[[_showNameAndState(_config)]]">
|
||||||
<div class="footer both">
|
<div class="footer both">
|
||||||
|
@ -78,6 +78,7 @@ class HuiPictureGlanceCard extends NavigateMixin(LocalizeMixin(EventsMixin(Polym
|
|||||||
state-image="[[_config.state_image]]"
|
state-image="[[_config.state_image]]"
|
||||||
camera-image="[[_config.camera_image]]"
|
camera-image="[[_config.camera_image]]"
|
||||||
entity="[[_config.entity]]"
|
entity="[[_config.entity]]"
|
||||||
|
aspect-ratio="[[_config.aspect_ratio]]"
|
||||||
></hui-image>
|
></hui-image>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<template is="dom-if" if="[[_config.title]]">
|
<template is="dom-if" if="[[_config.title]]">
|
||||||
|
@ -5,6 +5,8 @@ import '@polymer/paper-toggle-button/paper-toggle-button.js';
|
|||||||
import { STATES_OFF } from '../../../common/const.js';
|
import { STATES_OFF } from '../../../common/const.js';
|
||||||
import LocalizeMixin from '../../../mixins/localize-mixin.js';
|
import LocalizeMixin from '../../../mixins/localize-mixin.js';
|
||||||
|
|
||||||
|
import parseAspectRatio from '../../../common/util/parse-aspect-ratio.js';
|
||||||
|
|
||||||
const UPDATE_INTERVAL = 10000;
|
const UPDATE_INTERVAL = 10000;
|
||||||
const DEFAULT_FILTER = 'grayscale(100%)';
|
const DEFAULT_FILTER = 'grayscale(100%)';
|
||||||
|
|
||||||
@ -13,8 +15,23 @@ const DEFAULT_FILTER = 'grayscale(100%)';
|
|||||||
*/
|
*/
|
||||||
class HuiImage extends LocalizeMixin(PolymerElement) {
|
class HuiImage extends LocalizeMixin(PolymerElement) {
|
||||||
static get template() {
|
static get template() {
|
||||||
|
return html`
|
||||||
|
${this.styleTemplate}
|
||||||
|
<div id="wrapper">
|
||||||
|
<img
|
||||||
|
id="image"
|
||||||
|
src="[[_imageSrc]]"
|
||||||
|
on-error="_onImageError"
|
||||||
|
on-load="_onImageLoad" />
|
||||||
|
<div id="brokenImage"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styleTemplate() {
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
img {
|
img {
|
||||||
display: block;
|
display: block;
|
||||||
height: auto;
|
height: auto;
|
||||||
@ -30,19 +47,26 @@ class HuiImage extends LocalizeMixin(PolymerElement) {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ratio {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio img, .ratio div {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
#brokenImage {
|
#brokenImage {
|
||||||
background: grey url('/static/images/image-broken.svg') center/36px no-repeat;
|
background: grey url('/static/images/image-broken.svg') center/36px no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
`;
|
||||||
<img
|
|
||||||
id="image"
|
|
||||||
src="[[_imageSrc]]"
|
|
||||||
on-error="_onImageError"
|
|
||||||
on-load="_onImageLoad" />
|
|
||||||
<div id="brokenImage"></div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
@ -55,6 +79,7 @@ class HuiImage extends LocalizeMixin(PolymerElement) {
|
|||||||
image: String,
|
image: String,
|
||||||
stateImage: Object,
|
stateImage: Object,
|
||||||
cameraImage: String,
|
cameraImage: String,
|
||||||
|
aspectRatio: String,
|
||||||
filter: String,
|
filter: String,
|
||||||
stateFilter: Object,
|
stateFilter: Object,
|
||||||
_imageSrc: String
|
_imageSrc: String
|
||||||
@ -62,7 +87,7 @@ class HuiImage extends LocalizeMixin(PolymerElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get observers() {
|
static get observers() {
|
||||||
return ['_configChanged(image, stateImage, cameraImage)'];
|
return ['_configChanged(image, stateImage, cameraImage, aspectRatio)'];
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
@ -77,7 +102,14 @@ class HuiImage extends LocalizeMixin(PolymerElement) {
|
|||||||
clearInterval(this.timer);
|
clearInterval(this.timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
_configChanged(image, stateImage, cameraImage) {
|
_configChanged(image, stateImage, cameraImage, aspectRatio) {
|
||||||
|
const ratio = parseAspectRatio(aspectRatio);
|
||||||
|
|
||||||
|
if (ratio && ratio.w > 0 && ratio.h > 0) {
|
||||||
|
this.$.wrapper.style.paddingBottom = `${((100 * ratio.h) / ratio.w).toFixed(2)}%`;
|
||||||
|
this.$.wrapper.classList.add('ratio');
|
||||||
|
}
|
||||||
|
|
||||||
if (cameraImage) {
|
if (cameraImage) {
|
||||||
this._updateCameraImageSrc();
|
this._updateCameraImageSrc();
|
||||||
} else if (image && !stateImage) {
|
} else if (image && !stateImage) {
|
||||||
@ -88,15 +120,19 @@ class HuiImage extends LocalizeMixin(PolymerElement) {
|
|||||||
_onImageError() {
|
_onImageError() {
|
||||||
this._imageSrc = null;
|
this._imageSrc = null;
|
||||||
this.$.image.classList.add('hidden');
|
this.$.image.classList.add('hidden');
|
||||||
|
if (!this.$.wrapper.classList.contains('ratio')) {
|
||||||
this.$.brokenImage.style.setProperty('height', `${this._lastImageHeight || '100'}px`);
|
this.$.brokenImage.style.setProperty('height', `${this._lastImageHeight || '100'}px`);
|
||||||
|
}
|
||||||
this.$.brokenImage.classList.remove('hidden');
|
this.$.brokenImage.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
_onImageLoad() {
|
_onImageLoad() {
|
||||||
this.$.image.classList.remove('hidden');
|
this.$.image.classList.remove('hidden');
|
||||||
this.$.brokenImage.classList.add('hidden');
|
this.$.brokenImage.classList.add('hidden');
|
||||||
|
if (!this.$.wrapper.classList.contains('ratio')) {
|
||||||
this._lastImageHeight = this.$.image.offsetHeight;
|
this._lastImageHeight = this.$.image.offsetHeight;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_hassChanged(hass) {
|
_hassChanged(hass) {
|
||||||
if (this.cameraImage || !this.entity) {
|
if (this.cameraImage || !this.entity) {
|
||||||
|
@ -28,6 +28,7 @@ class HuiImageElement extends ElementClickMixin(PolymerElement) {
|
|||||||
filter="[[_config.filter]]"
|
filter="[[_config.filter]]"
|
||||||
state-filter="[[_config.state_filter]]"
|
state-filter="[[_config.state_filter]]"
|
||||||
title$="[[computeTooltip(hass, _config)]]"
|
title$="[[computeTooltip(hass, _config)]]"
|
||||||
|
aspect-ratio="[[_config.aspect_ratio]]"
|
||||||
></hui-image>
|
></hui-image>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
53
test-mocha/common/util/parse_aspect_ratio_test.js
Normal file
53
test-mocha/common/util/parse_aspect_ratio_test.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
|
import parseAspectRatio from '../../../src/common/util/parse-aspect-ratio.js';
|
||||||
|
|
||||||
|
describe('parseAspectRatio', () => {
|
||||||
|
const ratio16by9 = { w: 16, h: 9 };
|
||||||
|
const ratio178 = { w: 1.78, h: 1 };
|
||||||
|
|
||||||
|
it('Parses 16x9', () => {
|
||||||
|
const r = parseAspectRatio('16x9');
|
||||||
|
assert.deepEqual(r, ratio16by9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Parses 16:9', () => {
|
||||||
|
const r = parseAspectRatio('16:9');
|
||||||
|
assert.deepEqual(r, ratio16by9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Parses 1.78x1', () => {
|
||||||
|
const r = parseAspectRatio('1.78x1');
|
||||||
|
assert.deepEqual(r, ratio178);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Parses 1.78:1', () => {
|
||||||
|
const r = parseAspectRatio('1.78:1');
|
||||||
|
assert.deepEqual(r, ratio178);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Parses 1.78', () => {
|
||||||
|
const r = parseAspectRatio('1.78');
|
||||||
|
assert.deepEqual(r, ratio178);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Skips null states', () => {
|
||||||
|
const r = parseAspectRatio(null);
|
||||||
|
assert.equal(r, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Skips empty states', () => {
|
||||||
|
const r = parseAspectRatio(' ');
|
||||||
|
assert.equal(r, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Skips invalid input', () => {
|
||||||
|
const r = parseAspectRatio('mary had a little lamb');
|
||||||
|
assert.equal(r, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Skips invalid, but close input', () => {
|
||||||
|
const r = parseAspectRatio('mary:lamb');
|
||||||
|
assert.equal(r, null);
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user