Add a refresh loop for the image view itself.

This commit is contained in:
Dermot Duffy
2022-01-19 19:27:23 -08:00
parent aa0ee88970
commit 317a262f2b
6 changed files with 103 additions and 23 deletions
+2 -2
View File
@@ -338,6 +338,7 @@ image:
| Option | Default | Overridable | Description |
| - | - | - | - |
| `src` | | :heavy_multiplication_x: | [embedded image](https://www.flickr.com/photos/dianasch/47543120431) | A static image URL for use with the `image` [view](#views). Note that a `t=[timestsamp]` query parameter will be automatically added to this URL such that the image will not be cached by the browser. |
| `refresh_seconds` | 0 | :heavy_multiplication_x: | The number of seconds after which to refresh the image. `0` implies no refreshing. |
| `actions` | | :heavy_multiplication_x: | Actions to use for the `image` view. See [actions](#actions) below.|
### Dimension Options
@@ -1087,10 +1088,9 @@ This example fetches a static image every 10 seconds (in this case the latest im
[...]
view:
default: image
timeout: 10
update_force: true
image:
src: https://my-friage-server/api/living_room/latest.jpg
refresh_seconds: 10
```
</details>
+75 -12
View File
@@ -1,26 +1,86 @@
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import {
CSSResultGroup,
LitElement,
TemplateResult,
html,
unsafeCSS,
ReactiveController,
ReactiveControllerHost,
} from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { ImageViewConfig } from '../types.js';
import { View } from '../view.js';
import { dispatchMediaShowEvent } from '../common.js';
import type { ImageViewConfig } from '../types.js';
import defaultImage from '../images/frigate-bird-in-sky.jpg';
import imageStyle from '../scss/image.scss';
import { View } from '../view.js';
export class CachedValueController<T> implements ReactiveController {
public value?: T;
protected _host: ReactiveControllerHost;
protected _timerSeconds: number;
protected _callback: () => T;
protected _timerID?: number;
constructor(host: ReactiveControllerHost, timerSeconds: number, callback: () => T) {
(this._host = host).addController(this);
this._timerSeconds = timerSeconds;
this._callback = callback;
}
public removeController(): void {
this._host.removeController(this);
}
protected _updateValue(): void {
this.value = this._callback();
this._host.requestUpdate();
}
hostConnected(): void {
this._updateValue();
// Start a timer when the host is connected
if (this._timerSeconds > 0) {
this._timerID = window.setInterval(() => {
this._updateValue();
}, this._timerSeconds * 1000);
}
}
hostDisconnected(): void {
// Clear the timer when the host is disconnected
clearInterval(this._timerID);
this._timerID = undefined;
}
}
@customElement('frigate-card-image')
export class FrigateCardImage extends LitElement {
@property({ attribute: false })
protected imageConfig?: ImageViewConfig;
set imageConfig(imageConfig: ImageViewConfig) {
this._imageConfig = imageConfig;
if (this._cachedValueController) {
this._cachedValueController.removeController();
}
this._cachedValueController = new CachedValueController(
this,
this._imageConfig.refresh_seconds,
this._getImageSource.bind(this),
);
}
@state()
protected _imageConfig?: ImageViewConfig;
// A new view should trigger an image re-render.
@property({ attribute: false })
protected view?: Readonly<View>;
protected _cachedValueController?: CachedValueController<string>;
protected _getImageSource(): string {
if (this.imageConfig?.src) {
const url = new URL(this.imageConfig.src);
if (this._imageConfig?.src) {
const url = new URL(this._imageConfig.src);
url.searchParams.append('t', String(Date.now()));
return url.toString();
}
@@ -28,12 +88,15 @@ export class FrigateCardImage extends LitElement {
}
protected render(): TemplateResult | void {
return html` <img
src=${this._getImageSource()}
const src = this._cachedValueController?.value;
return src
? html` <img
src=${src}
@load=${(e) => {
dispatchMediaShowEvent(this, e);
}}
/>`;
/>`
: html``;
}
static get styles(): CSSResultGroup {
+1
View File
@@ -61,6 +61,7 @@ export const CONF_LIVE_WEBRTC_ENTITY = `${CONF_LIVE_WEBRTC}.entity` as const;
export const CONF_LIVE_WEBRTC_URL = `${CONF_LIVE_WEBRTC}.url` as const;
export const CONF_IMAGE = 'image' as const;
export const CONF_IMAGE_REFRESH_SECONDS = `${CONF_IMAGE}.refresh_seconds` as const;
export const CONF_IMAGE_SRC = `${CONF_IMAGE}.src` as const;
export const CONF_MENU = 'menu' as const;
+13 -4
View File
@@ -35,6 +35,7 @@ import {
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE,
CONF_EVENT_VIEWER_DRAGGABLE,
CONF_EVENT_VIEWER_LAZY_LOAD,
CONF_IMAGE_REFRESH_SECONDS,
CONF_IMAGE_SRC,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
@@ -488,16 +489,18 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
*/
protected _renderStringInput(
configPath: string,
allowedPattern?: string,
type?: 'text' | 'number',
): TemplateResult | void {
if (!this._config) {
return;
}
const allowedPattern = type == 'number' ? '[0-9]' : undefined
return html` <paper-input
label=${this._getLabel(configPath)}
.value=${getConfigValue(this._config, configPath, '')}
.configValue=${configPath}
allowed-pattern=${ifDefined(allowedPattern ? allowedPattern : undefined)}
type=${type || 'text'}
allowed-pattern=${ifDefined(allowedPattern)}
prevent-invalid-input=${ifDefined(allowedPattern)}
@change=${this._valueChangedHandler}
></paper-input>`;
@@ -654,7 +657,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
? html`
<div class="values">
${this._renderDropdown(CONF_VIEW_DEFAULT, viewModes)}
${this._renderStringInput(CONF_VIEW_TIMEOUT, '[0-9]')}
${this._renderStringInput(CONF_VIEW_TIMEOUT, 'number')}
${this._renderSwitch(CONF_VIEW_UPDATE_FORCE, defaults.view.update_force)}
</div>
`
@@ -779,7 +782,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
: ''}
${this._renderOptionSetHeader('image')}
${options.image.show
? html` <div class="values">${this._renderStringInput(CONF_IMAGE_SRC)}</div>`
? html` <div class="values">
${this._renderStringInput(CONF_IMAGE_SRC)}
${this._renderStringInput(CONF_IMAGE_REFRESH_SECONDS, 'number')}
</div>`
: ''}
${this._renderOptionSetHeader('dimensions')}
${options.dimensions.show
@@ -869,6 +875,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
value = target.checked;
} else if (typeof target.value === 'string') {
value = target.value?.trim();
if (target['type'] === 'number') {
value = Number(value);
}
} else {
value = target.value;
}
+2 -1
View File
@@ -98,7 +98,8 @@
}
},
"image": {
"src": "Static image URL/data for image view"
"src": "Static image URL/data for image view",
"refresh_seconds": "Number of seconds after which to refresh"
},
"menu": {
"buttons": {
+7 -1
View File
@@ -390,12 +390,17 @@ const viewConfigSchema = z
/**
* Image view configuration section.
*/
const imageConfigDefault = {
refresh_seconds: 0,
};
const imageConfigSchema = z
.object({
src: z.string().optional(),
refresh_seconds: z.number().min(0).default(imageConfigDefault.refresh_seconds)
})
.merge(actionsSchema)
.optional();
.default(imageConfigDefault);
export type ImageViewConfig = z.infer<typeof imageConfigSchema>;
/**
@@ -710,6 +715,7 @@ export const frigateCardConfigDefaults = {
live: liveConfigDefault,
event_viewer: viewerConfigDefault,
event_gallery: galleryConfigDefault,
image: imageConfigDefault,
};
const menuButtonSchema = z.union([