Support overriding live configuration per camera.
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
"embla-carousel": "^5.0.1",
|
"embla-carousel": "^5.0.1",
|
||||||
"home-assistant-js-websocket": "^5.11.1",
|
"home-assistant-js-websocket": "^5.11.1",
|
||||||
"lit": "^2.0.2",
|
"lit": "^2.0.2",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"quick-lru": "github:sindresorhus/quick-lru",
|
"quick-lru": "github:sindresorhus/quick-lru",
|
||||||
"screenfull": "^5.1.0",
|
"screenfull": "^5.1.0",
|
||||||
"zod": "^3.11.6"
|
"zod": "^3.11.6"
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
"@babel/plugin-proposal-decorators": "^7.15.8",
|
"@babel/plugin-proposal-decorators": "^7.15.8",
|
||||||
"@rollup/plugin-image": "^2.1.1",
|
"@rollup/plugin-image": "^2.1.1",
|
||||||
"@rollup/plugin-json": "^4.1.0",
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
|
"@types/lodash-es": "^4.17.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||||
"@typescript-eslint/parser": "^4.33.0",
|
"@typescript-eslint/parser": "^4.33.0",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
|
|||||||
+77
-10
@@ -1,10 +1,16 @@
|
|||||||
import type { FrigateCardCondition } from './types';
|
import type {
|
||||||
|
CameraConfig,
|
||||||
|
FrigateCardCondition,
|
||||||
|
RawFrigateCardConfig,
|
||||||
|
} from './types';
|
||||||
|
import { merge, cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
import { View } from './view';
|
import { View } from './view';
|
||||||
|
|
||||||
export interface ConditionState {
|
export interface ConditionState {
|
||||||
view?: Readonly<View>;
|
view?: Readonly<View>;
|
||||||
fullscreen?: boolean;
|
fullscreen?: boolean;
|
||||||
camera?: string;
|
camera?: CameraConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConditionStateRequestEvent extends Event {
|
class ConditionStateRequestEvent extends Event {
|
||||||
@@ -12,18 +18,49 @@ class ConditionStateRequestEvent extends Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function evaluateCondition(
|
export function evaluateCondition(
|
||||||
condition?: FrigateCardCondition,
|
condition?: Readonly<FrigateCardCondition>,
|
||||||
state?: ConditionState,
|
state?: Readonly<ConditionState>,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
if (!state) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let result = true;
|
let result = true;
|
||||||
if (condition?.view?.length && state?.view) {
|
if (condition?.view?.length && state.view) {
|
||||||
result &&= condition?.view.includes(state?.view.view);
|
result &&= condition?.view.includes(state.view.view);
|
||||||
}
|
}
|
||||||
if (condition?.fullscreen !== undefined && state?.fullscreen !== undefined) {
|
if (condition?.fullscreen !== undefined && state.fullscreen !== undefined) {
|
||||||
result &&= condition?.fullscreen == state?.fullscreen;
|
result &&= condition?.fullscreen == state.fullscreen;
|
||||||
}
|
}
|
||||||
if (condition?.camera?.length && state?.camera) {
|
|
||||||
result &&= condition?.camera.includes(state?.camera);
|
const evaluateNested = (
|
||||||
|
input: Readonly<RawFrigateCardConfig>,
|
||||||
|
condition: Readonly<RawFrigateCardConfig>,
|
||||||
|
): boolean => {
|
||||||
|
let result = true;
|
||||||
|
|
||||||
|
for (const key of Object.keys(condition)) {
|
||||||
|
if (typeof condition[key] === 'string') {
|
||||||
|
// If the test is a literal, it must exactly match.
|
||||||
|
result &&= input[key] === condition[key];
|
||||||
|
} else if (Array.isArray(condition[key])) {
|
||||||
|
// If the test is an array, it's a list of acceptable values.
|
||||||
|
result &&= (condition[key] as unknown[]).includes(input[key]);
|
||||||
|
} else if (typeof condition[key] === 'object' && typeof input[key] === 'object') {
|
||||||
|
// If the test is an object, recursively navigate downwards.
|
||||||
|
result &&= evaluateNested(
|
||||||
|
input[key] as RawFrigateCardConfig,
|
||||||
|
condition[key] as RawFrigateCardConfig,
|
||||||
|
);
|
||||||
|
} else if (input[key] === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (condition?.camera) {
|
||||||
|
result &&= state.camera ? evaluateNested(state.camera, condition.camera) : false;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -70,3 +107,33 @@ export function conditionStateRequestHandler(
|
|||||||
): void {
|
): void {
|
||||||
ev.conditionState = conditionState;
|
ev.conditionState = conditionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Overrides = {
|
||||||
|
conditions: FrigateCardCondition;
|
||||||
|
overrides: RawFrigateCardConfig;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export function getOverriddenConfig(
|
||||||
|
config: Readonly<RawFrigateCardConfig>,
|
||||||
|
conditionState?: Readonly<ConditionState>,
|
||||||
|
overrides?: Readonly<Overrides>,
|
||||||
|
): RawFrigateCardConfig {
|
||||||
|
const overridesSource =
|
||||||
|
overrides || (config['overrides'] as Readonly<Overrides> | undefined);
|
||||||
|
if (!overridesSource) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = cloneDeep(config);
|
||||||
|
let overridden = false;
|
||||||
|
|
||||||
|
for (const override of overridesSource) {
|
||||||
|
if (evaluateCondition(override.conditions, conditionState)) {
|
||||||
|
merge(output, override.overrides);
|
||||||
|
overridden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Attempt to return the same configuration object if it has not been
|
||||||
|
// overridden (to reduce re-renders for a configuration that has not changed).
|
||||||
|
return overridden ? output : config;
|
||||||
|
}
|
||||||
+16
-5
@@ -22,13 +22,14 @@ import screenfull from 'screenfull';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Actions,
|
||||||
ActionType,
|
ActionType,
|
||||||
|
CameraConfig,
|
||||||
GetFrigateCardMenuButtonParameters,
|
GetFrigateCardMenuButtonParameters,
|
||||||
|
LiveConfig,
|
||||||
RawFrigateCardConfig,
|
RawFrigateCardConfig,
|
||||||
entitySchema,
|
entitySchema,
|
||||||
frigateCardConfigSchema,
|
frigateCardConfigSchema,
|
||||||
Actions,
|
|
||||||
CameraConfig,
|
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import type {
|
import type {
|
||||||
Entity,
|
Entity,
|
||||||
@@ -74,7 +75,11 @@ import { ResolvedMediaCache } from './resolved-media.js';
|
|||||||
import { BrowseMediaUtil } from './browse-media-util.js';
|
import { BrowseMediaUtil } from './browse-media-util.js';
|
||||||
import { isConfigUpgradeable } from './config-mgmt.js';
|
import { isConfigUpgradeable } from './config-mgmt.js';
|
||||||
import { actionHandler } from './action-handler-directive.js';
|
import { actionHandler } from './action-handler-directive.js';
|
||||||
import { ConditionState, conditionStateRequestHandler } from './card-condition.js';
|
import {
|
||||||
|
ConditionState,
|
||||||
|
conditionStateRequestHandler,
|
||||||
|
getOverriddenConfig,
|
||||||
|
} from './card-condition.js';
|
||||||
|
|
||||||
/** A note on media callbacks:
|
/** A note on media callbacks:
|
||||||
*
|
*
|
||||||
@@ -219,7 +224,8 @@ export class FrigateCard extends LitElement {
|
|||||||
this._conditionState = {
|
this._conditionState = {
|
||||||
view: this._view,
|
view: this._view,
|
||||||
fullscreen: screenfull.isEnabled && screenfull.isFullscreen,
|
fullscreen: screenfull.isEnabled && screenfull.isFullscreen,
|
||||||
camera: this._view?.camera,
|
camera:
|
||||||
|
this._cameras && this._view ? this._cameras.get(this._view.camera) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1035,7 +1041,11 @@ export class FrigateCard extends LitElement {
|
|||||||
let specificActions: Actions | undefined = undefined;
|
let specificActions: Actions | undefined = undefined;
|
||||||
|
|
||||||
if (this._view?.is('live')) {
|
if (this._view?.is('live')) {
|
||||||
specificActions = this.config.live.actions;
|
const config = getOverriddenConfig(
|
||||||
|
this.config.live,
|
||||||
|
this._conditionState,
|
||||||
|
) as LiveConfig;
|
||||||
|
specificActions = config.actions;
|
||||||
} else if (this._view?.isGalleryView()) {
|
} else if (this._view?.isGalleryView()) {
|
||||||
specificActions = this.config.event_gallery?.actions;
|
specificActions = this.config.event_gallery?.actions;
|
||||||
} else if (this._view?.isViewerView()) {
|
} else if (this._view?.isViewerView()) {
|
||||||
@@ -1198,6 +1208,7 @@ export class FrigateCard extends LitElement {
|
|||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.view=${this._view}
|
.view=${this._view}
|
||||||
.liveConfig=${this.config.live}
|
.liveConfig=${this.config.live}
|
||||||
|
.conditionState=${this._conditionState}
|
||||||
.cameras=${this._cameras}
|
.cameras=${this._cameras}
|
||||||
.preload=${this.config.live.preload && !this._view.is('live')}
|
.preload=${this.config.live.preload && !this._view.is('live')}
|
||||||
class="${classMap(liveClasses)}"
|
class="${classMap(liveClasses)}"
|
||||||
|
|||||||
+73
-18
@@ -1,5 +1,9 @@
|
|||||||
// TODO update_entities should reload view rather than be involved in rendering
|
|
||||||
// TODO different live configs per camera
|
// TODO different live configs per camera
|
||||||
|
// TODO replace dset/dlv with lodash
|
||||||
|
// TODO evaluate single override section instead of per-subcomponent
|
||||||
|
// TODO fix adaptive height in fullscreen
|
||||||
|
// TODO Remove id as a concept for cameras and use an array instead?
|
||||||
|
// TODO Convert menu condition to use overrides
|
||||||
// TODO Remove media load event console message
|
// TODO Remove media load event console message
|
||||||
// TODO Remove view change console message
|
// TODO Remove view change console message
|
||||||
// TODO readme
|
// TODO readme
|
||||||
@@ -26,10 +30,12 @@ import {
|
|||||||
import { EmblaOptionsType } from 'embla-carousel';
|
import { EmblaOptionsType } from 'embla-carousel';
|
||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
import { ref } from 'lit/directives/ref';
|
import { ref } from 'lit/directives/ref';
|
||||||
import { until } from 'lit/directives/until.js';
|
import { until } from 'lit/directives/until.js';
|
||||||
|
|
||||||
import { BrowseMediaUtil } from '../browse-media-util.js';
|
import { BrowseMediaUtil } from '../browse-media-util.js';
|
||||||
|
import { ConditionState, getOverriddenConfig } from '../card-condition.js';
|
||||||
import { FrigateCardMediaCarousel } from './media-carousel.js';
|
import { FrigateCardMediaCarousel } from './media-carousel.js';
|
||||||
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
|
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
|
||||||
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
||||||
@@ -75,6 +81,9 @@ export class FrigateCardLive extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected liveConfig?: LiveConfig;
|
protected liveConfig?: LiveConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected conditionState?: ConditionState;
|
||||||
|
|
||||||
set preload(preload: boolean) {
|
set preload(preload: boolean) {
|
||||||
this._preload = preload;
|
this._preload = preload;
|
||||||
|
|
||||||
@@ -108,17 +117,17 @@ export class FrigateCardLive extends LitElement {
|
|||||||
* Render thumbnails carousel.
|
* Render thumbnails carousel.
|
||||||
* @returns A rendered template or void.
|
* @returns A rendered template or void.
|
||||||
*/
|
*/
|
||||||
protected renderThumbnails(): TemplateResult | void {
|
protected renderThumbnails(config: LiveConfig): TemplateResult | void {
|
||||||
if (!this.liveConfig || !this.view) {
|
if (!this.liveConfig || !this.view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchThumbnailsThenRender = async (): Promise<TemplateResult | void> => {
|
const fetchThumbnailsThenRender = async (): Promise<TemplateResult | void> => {
|
||||||
if (!this.hass || !this.cameras || !this.view || !this.liveConfig) {
|
if (!this.hass || !this.cameras || !this.view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const browseMediaParams = BrowseMediaUtil.getBrowseMediaQueryParameters(
|
const browseMediaParams = BrowseMediaUtil.getBrowseMediaQueryParameters(
|
||||||
this.liveConfig.controls.thumbnails.media,
|
config.controls.thumbnails.media,
|
||||||
this.cameras.get(this.view.camera),
|
this.cameras.get(this.view.camera),
|
||||||
);
|
);
|
||||||
if (!browseMediaParams) {
|
if (!browseMediaParams) {
|
||||||
@@ -135,7 +144,7 @@ export class FrigateCardLive extends LitElement {
|
|||||||
return html` <frigate-card-thumbnail-carousel
|
return html` <frigate-card-thumbnail-carousel
|
||||||
.target=${parent}
|
.target=${parent}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.config=${this.liveConfig?.controls.thumbnails}
|
.config=${config.controls.thumbnails}
|
||||||
.highlightSelected=${false}
|
.highlightSelected=${false}
|
||||||
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
|
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
|
||||||
const mediaType = browseMediaParams.mediaType;
|
const mediaType = browseMediaParams.mediaType;
|
||||||
@@ -165,16 +174,23 @@ export class FrigateCardLive extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const config = getOverriddenConfig(
|
||||||
|
this.liveConfig,
|
||||||
|
this.conditionState,
|
||||||
|
) as LiveConfig;
|
||||||
|
|
||||||
|
// Note use of liveConfig and not config below -- the carousel will
|
||||||
|
// independently override the liveconfig to reflect the camera in the
|
||||||
|
// carousel (not necessarily the selected camera).
|
||||||
return html`
|
return html`
|
||||||
${this.liveConfig.controls.thumbnails.mode === 'above'
|
${config.controls.thumbnails.mode === 'above' ? this.renderThumbnails(config) : ''}
|
||||||
? this.renderThumbnails()
|
|
||||||
: ''}
|
|
||||||
<frigate-card-live-carousel
|
<frigate-card-live-carousel
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.cameras=${this.cameras}
|
.cameras=${this.cameras}
|
||||||
.liveConfig=${this.liveConfig}
|
.liveConfig=${this.liveConfig}
|
||||||
.preload=${this._preload}
|
.preload=${this._preload}
|
||||||
|
.conditionState=${this.conditionState}
|
||||||
@frigate-card:media-show=${this._mediaShowHandler}
|
@frigate-card:media-show=${this._mediaShowHandler}
|
||||||
@frigate-card:carousel:select=${() => {
|
@frigate-card:carousel:select=${() => {
|
||||||
// Re-rendering the component will cause the thumbnails to be
|
// Re-rendering the component will cause the thumbnails to be
|
||||||
@@ -183,9 +199,7 @@ export class FrigateCardLive extends LitElement {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
</frigate-card-live-carousel>
|
</frigate-card-live-carousel>
|
||||||
${this.liveConfig.controls.thumbnails.mode === 'below'
|
${config.controls.thumbnails.mode === 'below' ? this.renderThumbnails(config) : ''}
|
||||||
? this.renderThumbnails()
|
|
||||||
: ''}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +228,9 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected preload?: boolean;
|
protected preload?: boolean;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected conditionState?: ConditionState;
|
||||||
|
|
||||||
// Index between camera name and slide number.
|
// Index between camera name and slide number.
|
||||||
protected _cameraToSlide: Record<string, number> = {};
|
protected _cameraToSlide: Record<string, number> = {};
|
||||||
|
|
||||||
@@ -332,13 +349,29 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _renderLive(cameraConfig: CameraConfig, slideIndex: number): TemplateResult {
|
protected _renderLive(
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
slideIndex: number,
|
||||||
|
): TemplateResult | void {
|
||||||
|
if (!this.liveConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The conditionState object contains the currently live camera, which (in
|
||||||
|
// the carousel for example) is not necessarily the live camera this
|
||||||
|
// <frigate-card-live-provider> is rendering right now.
|
||||||
|
const conditionState = Object.assign({
|
||||||
|
...this.conditionState,
|
||||||
|
camera: cameraConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = getOverriddenConfig(this.liveConfig, conditionState) as LiveConfig;
|
||||||
|
|
||||||
return html` <div class="embla__slide">
|
return html` <div class="embla__slide">
|
||||||
<frigate-card-live-provider
|
<frigate-card-live-provider
|
||||||
.title=${getCameraTitle(this.hass, cameraConfig)}
|
.title=${getCameraTitle(this.hass, cameraConfig)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${cameraConfig}
|
.cameraConfig=${cameraConfig}
|
||||||
.liveConfig=${this.liveConfig}
|
.liveConfig=${config}
|
||||||
?disabled=${this._isLazyLoading()}
|
?disabled=${this._isLazyLoading()}
|
||||||
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) =>
|
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) =>
|
||||||
this._mediaShowEventHandler(slideIndex, e)}
|
this._mediaShowEventHandler(slideIndex, e)}
|
||||||
@@ -400,17 +433,22 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
|||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
const [slides, cameraToSlide] = this._getSlides();
|
const [slides, cameraToSlide] = this._getSlides();
|
||||||
this._cameraToSlide = cameraToSlide;
|
this._cameraToSlide = cameraToSlide;
|
||||||
if (!slides) {
|
if (!slides || !this.liveConfig) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const config = getOverriddenConfig(
|
||||||
|
this.liveConfig,
|
||||||
|
this.conditionState,
|
||||||
|
) as LiveConfig;
|
||||||
|
|
||||||
const [prev, next] = this._getCameraNeighbors();
|
const [prev, next] = this._getCameraNeighbors();
|
||||||
return html`
|
return html`
|
||||||
<div class="embla">
|
<div class="embla">
|
||||||
<frigate-card-next-previous-control
|
<frigate-card-next-previous-control
|
||||||
${ref(this._previousControlRef)}
|
${ref(this._previousControlRef)}
|
||||||
.direction=${'previous'}
|
.direction=${'previous'}
|
||||||
.controlConfig=${this.liveConfig?.controls.next_previous}
|
.controlConfig=${config.controls.next_previous}
|
||||||
.title=${getCameraTitle(this.hass, prev)}
|
.title=${getCameraTitle(this.hass, prev)}
|
||||||
.icon=${getCameraIcon(this.hass, prev)}
|
.icon=${getCameraIcon(this.hass, prev)}
|
||||||
?disabled=${prev == null}
|
?disabled=${prev == null}
|
||||||
@@ -425,7 +463,7 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
|||||||
<frigate-card-next-previous-control
|
<frigate-card-next-previous-control
|
||||||
${ref(this._nextControlRef)}
|
${ref(this._nextControlRef)}
|
||||||
.direction=${'next'}
|
.direction=${'next'}
|
||||||
.controlConfig=${this.liveConfig?.controls.next_previous}
|
.controlConfig=${config.controls.next_previous}
|
||||||
.title=${getCameraTitle(this.hass, next)}
|
.title=${getCameraTitle(this.hass, next)}
|
||||||
.icon=${getCameraIcon(this.hass, next)}
|
.icon=${getCameraIcon(this.hass, next)}
|
||||||
?disabled=${next == null}
|
?disabled=${next == null}
|
||||||
@@ -533,7 +571,18 @@ export class FrigateCardLiveFrigate extends LitElement {
|
|||||||
// - https://github.com/AlexxIT/WebRTC
|
// - https://github.com/AlexxIT/WebRTC
|
||||||
@customElement('frigate-card-live-webrtc')
|
@customElement('frigate-card-live-webrtc')
|
||||||
export class FrigateCardLiveWebRTC extends LitElement {
|
export class FrigateCardLiveWebRTC extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({
|
||||||
|
attribute: false,
|
||||||
|
|
||||||
|
// Resetting the WebRTC/JSMPEG configuration is expensive as the connections
|
||||||
|
// need to be re-established. These configurations may be overridden which
|
||||||
|
// creates semantically equal configurations at different addresses --
|
||||||
|
// ensure LIT only considers the property as having changed if it's actually
|
||||||
|
// different.
|
||||||
|
hasChanged(n: WebRTCConfig, o: WebRTCConfig): boolean {
|
||||||
|
return !isEqual(n, o);
|
||||||
|
},
|
||||||
|
})
|
||||||
protected webRTCConfig?: WebRTCConfig;
|
protected webRTCConfig?: WebRTCConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -638,7 +687,13 @@ export class FrigateCardLiveJSMPEG extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected cameraConfig?: CameraConfig;
|
protected cameraConfig?: CameraConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({
|
||||||
|
attribute: false,
|
||||||
|
// See note under FrigateCardLiveWebRTC.
|
||||||
|
hasChanged(n: JSMPEGConfig, o: JSMPEGConfig): boolean {
|
||||||
|
return !isEqual(n, o);
|
||||||
|
},
|
||||||
|
})
|
||||||
protected jsmpegConfig?: JSMPEGConfig;
|
protected jsmpegConfig?: JSMPEGConfig;
|
||||||
|
|
||||||
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
|
|
||||||
import { NextPreviousControlConfig } from '../types.js';
|
import { NextPreviousControlConfig } from '../types.js';
|
||||||
@@ -11,13 +11,14 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public direction?: 'next' | 'previous';
|
public direction?: 'next' | 'previous';
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
set controlConfig(controlConfig: NextPreviousControlConfig | undefined) {
|
set controlConfig(controlConfig: NextPreviousControlConfig | undefined) {
|
||||||
if (controlConfig?.size) {
|
if (controlConfig?.size) {
|
||||||
this.style.setProperty('--frigate-card-next-prev-size', controlConfig.size);
|
this.style.setProperty('--frigate-card-next-prev-size', controlConfig.size);
|
||||||
}
|
}
|
||||||
this._controlConfig = controlConfig;
|
this._controlConfig = controlConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@state()
|
||||||
protected _controlConfig?: NextPreviousControlConfig;
|
protected _controlConfig?: NextPreviousControlConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
|
|||||||
+1
-1
@@ -193,7 +193,7 @@ const upgradeMoveTo = function (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitize a potentially-unsafe key segment.
|
* Upgrade from a singular camera model to multiple.
|
||||||
* @param key A string key.
|
* @param key A string key.
|
||||||
* @returns A safe key.
|
* @returns A safe key.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+68
-50
@@ -1,62 +1,80 @@
|
|||||||
export const CARD_VERSION = '2.1.0';
|
export const CARD_VERSION = '2.1.0' as const;
|
||||||
export const REPO_URL = 'https://github.com/dermotduffy/frigate-hass-card';
|
export const REPO_URL = 'https://github.com/dermotduffy/frigate-hass-card' as const;
|
||||||
export const TROUBLESHOOTING_URL = `${REPO_URL}#troubleshooting`;
|
export const TROUBLESHOOTING_URL = `${REPO_URL}#troubleshooting` as const;
|
||||||
|
|
||||||
export const CONF_CAMERAS = 'cameras';
|
export const CONF_CAMERAS = 'cameras' as const;
|
||||||
export const CONF_CAMERAS_ARRAY_CAMERA_ENTITY = 'cameras.#.camera_entity';
|
export const CONF_CAMERAS_ARRAY_CAMERA_ENTITY =
|
||||||
export const CONF_CAMERAS_ARRAY_CAMERA_NAME = 'cameras.#.camera_name';
|
`${CONF_CAMERAS}.#.camera_entity` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_CLIENT_ID = 'cameras.#.client_id';
|
export const CONF_CAMERAS_ARRAY_CAMERA_NAME = `${CONF_CAMERAS}.#.camera_name` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_LABEL = 'cameras.#.label';
|
export const CONF_CAMERAS_ARRAY_CLIENT_ID = `${CONF_CAMERAS}.#.client_id` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_URL = 'cameras.#.frigate_url';
|
export const CONF_CAMERAS_ARRAY_LABEL = `${CONF_CAMERAS}.#.label` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_ZONE = 'cameras.#.zone';
|
export const CONF_CAMERAS_ARRAY_URL = `${CONF_CAMERAS}.#.frigate_url` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_ID = 'cameras.#.id';
|
export const CONF_CAMERAS_ARRAY_ZONE = `${CONF_CAMERAS}.#.zone` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_TITLE = 'cameras.#.title';
|
export const CONF_CAMERAS_ARRAY_ID = `${CONF_CAMERAS}.#.id` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_ICON = 'cameras.#.icon';
|
export const CONF_CAMERAS_ARRAY_TITLE = `${CONF_CAMERAS}.#.title` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_WEBRTC_ENTITY = 'cameras.#.webrtc.entity';
|
export const CONF_CAMERAS_ARRAY_ICON = `${CONF_CAMERAS}.#.icon` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_WEBRTC_URL = 'cameras.#.webrtc.url';
|
export const CONF_CAMERAS_ARRAY_WEBRTC_ENTITY =
|
||||||
|
`${CONF_CAMERAS}.#.webrtc.entity` as const;
|
||||||
|
export const CONF_CAMERAS_ARRAY_WEBRTC_URL = `${CONF_CAMERAS}.#.webrtc.url` as const;
|
||||||
|
|
||||||
export const CONF_VIEW_DEFAULT = 'view.default';
|
export const CONF_VIEW = 'view' as const;
|
||||||
export const CONF_VIEW_TIMEOUT = 'view.timeout';
|
export const CONF_VIEW_DEFAULT = `${CONF_VIEW}.default` as const;
|
||||||
export const CONF_VIEW_UPDATE_FORCE = 'view.update_force';
|
export const CONF_VIEW_TIMEOUT = `${CONF_VIEW}.timeout` as const;
|
||||||
export const CONF_VIEW_UPDATE_ENTITIES = 'view.update_entities';
|
export const CONF_VIEW_UPDATE_FORCE = `${CONF_VIEW}.update_force` as const;
|
||||||
|
export const CONF_VIEW_UPDATE_ENTITIES = `${CONF_VIEW}.update_entities` as const;
|
||||||
|
|
||||||
export const CONF_EVENT_VIEWER_AUTOPLAY_CLIP = 'event_viewer.autoplay_clip';
|
export const CONF_EVENT_VIEWER = 'event_viewer' as const;
|
||||||
export const CONF_EVENT_VIEWER_DRAGGABLE = 'event_viewer.draggable';
|
export const CONF_EVENT_VIEWER_AUTOPLAY_CLIP =
|
||||||
export const CONF_EVENT_VIEWER_LAZY_LOAD = 'event_viewer.lazy_load';
|
`${CONF_EVENT_VIEWER}.autoplay_clip` as const;
|
||||||
|
export const CONF_EVENT_VIEWER_DRAGGABLE = `${CONF_EVENT_VIEWER}.draggable` as const;
|
||||||
|
export const CONF_EVENT_VIEWER_LAZY_LOAD = `${CONF_EVENT_VIEWER}.lazy_load` as const;
|
||||||
export const CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE =
|
export const CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE =
|
||||||
'event_viewer.controls.next_previous.style';
|
`${CONF_EVENT_VIEWER}.controls.next_previous.style` as const;
|
||||||
export const CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE =
|
export const CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE =
|
||||||
'event_viewer.controls.next_previous.size';
|
`${CONF_EVENT_VIEWER}.controls.next_previous.size` as const;
|
||||||
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE =
|
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE =
|
||||||
'event_viewer.controls.thumbnails.mode';
|
`${CONF_EVENT_VIEWER}.controls.thumbnails.mode` as const;
|
||||||
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE =
|
export const CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE =
|
||||||
'event_viewer.controls.thumbnails.size';
|
`${CONF_EVENT_VIEWER}.controls.thumbnails.size` as const;
|
||||||
|
|
||||||
|
export const CONF_LIVE = 'live' as const;
|
||||||
export const CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE =
|
export const CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE =
|
||||||
'live.controls.next_previous.style';
|
`${CONF_LIVE}.controls.next_previous.style` as const;
|
||||||
export const CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE = 'live.controls.next_previous.size';
|
export const CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE =
|
||||||
export const CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA = 'live.controls.thumbnails.media';
|
`${CONF_LIVE}.controls.next_previous.size` as const;
|
||||||
export const CONF_LIVE_CONTROLS_THUMBNAILS_MODE = 'live.controls.thumbnails.mode';
|
export const CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA =
|
||||||
export const CONF_LIVE_CONTROLS_THUMBNAILS_SIZE = 'live.controls.thumbnails.size';
|
`${CONF_LIVE}.controls.thumbnails.media` as const;
|
||||||
export const CONF_LIVE_DRAGGABLE = 'live.draggable';
|
export const CONF_LIVE_CONTROLS_THUMBNAILS_MODE =
|
||||||
export const CONF_LIVE_LAZY_LOAD = 'live.lazy_load';
|
`${CONF_LIVE}.controls.thumbnails.mode` as const;
|
||||||
export const CONF_LIVE_PRELOAD = 'live.preload';
|
export const CONF_LIVE_CONTROLS_THUMBNAILS_SIZE =
|
||||||
export const CONF_LIVE_PROVIDER = 'live.provider';
|
`${CONF_LIVE}.controls.thumbnails.size` as const;
|
||||||
export const CONF_LIVE_WEBRTC_ENTITY = 'live.webrtc.entity';
|
export const CONF_LIVE_DRAGGABLE = `${CONF_LIVE}.draggable` as const;
|
||||||
export const CONF_LIVE_WEBRTC_URL = 'live.webrtc.url';
|
export const CONF_LIVE_JSMPEG = `${CONF_LIVE}.jsmpeg` as const;
|
||||||
|
export const CONF_LIVE_LAZY_LOAD = `${CONF_LIVE}.lazy_load` as const;
|
||||||
|
export const CONF_LIVE_PRELOAD = `${CONF_LIVE}.preload` as const;
|
||||||
|
export const CONF_LIVE_PROVIDER = `${CONF_LIVE}.provider` as const;
|
||||||
|
export const CONF_LIVE_WEBRTC = `${CONF_LIVE}.webrtc` as const;
|
||||||
|
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_SRC = 'image.src';
|
export const CONF_IMAGE = 'image' as const;
|
||||||
|
export const CONF_IMAGE_SRC = `${CONF_IMAGE}.src` as const;
|
||||||
|
|
||||||
export const CONF_MENU_BUTTONS_FRIGATE = 'menu.buttons.frigate';
|
export const CONF_MENU = 'menu' as const;
|
||||||
export const CONF_MENU_BUTTONS_FRIGATE_UI = 'menu.buttons.frigate_ui';
|
export const CONF_MENU_BUTTONS_FRIGATE = `${CONF_MENU}.buttons.frigate` as const;
|
||||||
export const CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN = 'menu.buttons.fullscreen';
|
export const CONF_MENU_BUTTONS_FRIGATE_UI = `${CONF_MENU}.buttons.frigate_ui` as const;
|
||||||
export const CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD = 'menu.buttons.download';
|
export const CONF_MENU_BUTTONS_FRIGATE_FULLSCREEN =
|
||||||
export const CONF_MENU_BUTTONS_LIVE = 'menu.buttons.live';
|
`${CONF_MENU}.buttons.fullscreen` as const;
|
||||||
export const CONF_MENU_BUTTONS_CLIPS = 'menu.buttons.clips';
|
export const CONF_MENU_BUTTONS_FRIGATE_DOWNLOAD =
|
||||||
export const CONF_MENU_BUTTONS_SNAPSHOTS = 'menu.buttons.snapshots';
|
`${CONF_MENU}.buttons.download` as const;
|
||||||
export const CONF_MENU_BUTTONS_IMAGE = 'menu.buttons.image';
|
export const CONF_MENU_BUTTONS_LIVE = `${CONF_MENU}.buttons.live` as const;
|
||||||
export const CONF_MENU_BUTTON_SIZE = 'menu.button_size';
|
export const CONF_MENU_BUTTONS_CLIPS = `${CONF_MENU}.buttons.clips` as const;
|
||||||
export const CONF_MENU_MODE = 'menu.mode';
|
export const CONF_MENU_BUTTONS_SNAPSHOTS = `${CONF_MENU}.buttons.snapshots` as const;
|
||||||
|
export const CONF_MENU_BUTTONS_IMAGE = `${CONF_MENU}.buttons.image` as const;
|
||||||
|
export const CONF_MENU_BUTTON_SIZE = `${CONF_MENU}.button_size` as const;
|
||||||
|
export const CONF_MENU_MODE = `${CONF_MENU}.mode` as const;
|
||||||
|
|
||||||
export const CONF_DIMENSIONS_ASPECT_RATIO = 'dimensions.aspect_ratio';
|
export const CONF_DIMENSIONS = 'dimensions' as const;
|
||||||
export const CONF_DIMENSIONS_ASPECT_RATIO_MODE = 'dimensions.aspect_ratio_mode';
|
export const CONF_DIMENSIONS_ASPECT_RATIO = `${CONF_DIMENSIONS}.aspect_ratio` as const;
|
||||||
|
export const CONF_DIMENSIONS_ASPECT_RATIO_MODE =
|
||||||
|
`${CONF_DIMENSIONS}.aspect_ratio_mode` as const;
|
||||||
|
|||||||
+114
-61
@@ -276,6 +276,41 @@ const customSchema = z
|
|||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Camera configuration section
|
||||||
|
*/
|
||||||
|
export const cameraConfigDefault = {
|
||||||
|
client_id: 'frigate' as const,
|
||||||
|
};
|
||||||
|
const webrtcCameraConfigSchema = z.object({
|
||||||
|
entity: z.string().optional(),
|
||||||
|
url: z.string().optional(),
|
||||||
|
});
|
||||||
|
const cameraConfigSchema = z
|
||||||
|
.object({
|
||||||
|
// No URL validation to allow relative URLs within HA (e.g. Frigate addon).
|
||||||
|
frigate_url: z.string().optional(),
|
||||||
|
client_id: z.string().optional().default(cameraConfigDefault.client_id),
|
||||||
|
camera_name: z.string().optional(),
|
||||||
|
label: z.string().optional(),
|
||||||
|
zone: z.string().optional(),
|
||||||
|
camera_entity: z.string().optional(),
|
||||||
|
|
||||||
|
// Used for presentation in the UI (autodetected from the entity if
|
||||||
|
// specified).
|
||||||
|
icon: z.string().optional(),
|
||||||
|
title: z.string().optional(),
|
||||||
|
|
||||||
|
// Optional identifier to separate different camera configurations used in
|
||||||
|
// this card.
|
||||||
|
id: z.string().optional(),
|
||||||
|
|
||||||
|
// Camera identifiers for WebRTC.
|
||||||
|
webrtc: webrtcCameraConfigSchema.optional(),
|
||||||
|
})
|
||||||
|
.default(cameraConfigDefault);
|
||||||
|
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Element Types.
|
* Custom Element Types.
|
||||||
*/
|
*/
|
||||||
@@ -314,7 +349,9 @@ export type MenuSubmenu = z.infer<typeof menuSubmenuSchema>;
|
|||||||
const frigateCardConditionSchema = z.object({
|
const frigateCardConditionSchema = z.object({
|
||||||
view: z.string().array().optional(),
|
view: z.string().array().optional(),
|
||||||
fullscreen: z.boolean().optional(),
|
fullscreen: z.boolean().optional(),
|
||||||
camera: z.string().array().optional(),
|
|
||||||
|
// Allow matching any field of cameraConfig.
|
||||||
|
camera: z.record(z.any()),
|
||||||
});
|
});
|
||||||
export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>;
|
export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>;
|
||||||
|
|
||||||
@@ -345,39 +382,16 @@ const pictureElementsSchema = pictureElementSchema.array().optional();
|
|||||||
export type PictureElements = z.infer<typeof pictureElementsSchema>;
|
export type PictureElements = z.infer<typeof pictureElementsSchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Camera configuration section
|
* Configuration overrides
|
||||||
*/
|
*/
|
||||||
export const cameraConfigDefault = {
|
const overridesSchema = z
|
||||||
client_id: 'frigate' as const,
|
|
||||||
};
|
|
||||||
const webrtcCameraConfigSchema = z.object({
|
|
||||||
entity: z.string().optional(),
|
|
||||||
url: z.string().optional(),
|
|
||||||
});
|
|
||||||
const cameraConfigDefaultSchema = z
|
|
||||||
.object({
|
.object({
|
||||||
// No URL validation to allow relative URLs within HA (e.g. Frigate addon).
|
conditions: frigateCardConditionSchema,
|
||||||
frigate_url: z.string().optional(),
|
overrides: z.record(z.unknown()),
|
||||||
client_id: z.string().optional().default(cameraConfigDefault.client_id),
|
|
||||||
camera_name: z.string().optional(),
|
|
||||||
label: z.string().optional(),
|
|
||||||
zone: z.string().optional(),
|
|
||||||
camera_entity: z.string().optional(),
|
|
||||||
|
|
||||||
// Used for presentation in the UI (autodetected from the entity if
|
|
||||||
// specified).
|
|
||||||
icon: z.string().optional(),
|
|
||||||
title: z.string().optional(),
|
|
||||||
|
|
||||||
// Optional identifier to separate different camera configurations used in
|
|
||||||
// this card.
|
|
||||||
id: z.string().optional(),
|
|
||||||
|
|
||||||
// Camera identifiers for WebRTC.
|
|
||||||
webrtc: webrtcCameraConfigSchema.optional(),
|
|
||||||
})
|
})
|
||||||
.default(cameraConfigDefault);
|
.array()
|
||||||
export type CameraConfig = z.infer<typeof cameraConfigDefaultSchema>;
|
.optional();
|
||||||
|
export type Overrides = z.infer<typeof overridesSchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View configuration section.
|
* View configuration section.
|
||||||
@@ -424,12 +438,8 @@ export type ImageViewConfig = z.infer<typeof imageConfigSchema>;
|
|||||||
* Thumbnail controls configuration section.
|
* Thumbnail controls configuration section.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const thumbnailsControlDefault = {
|
|
||||||
mode: 'none' as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
const thumbnailsControlSchema = z.object({
|
const thumbnailsControlSchema = z.object({
|
||||||
mode: z.enum(['none', 'above', 'below']).default(thumbnailsControlDefault.mode),
|
mode: z.enum(['none', 'above', 'below']),
|
||||||
size: z.string().optional(),
|
size: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type ThumbnailsControlConfig = z.infer<typeof thumbnailsControlSchema>;
|
export type ThumbnailsControlConfig = z.infer<typeof thumbnailsControlSchema>;
|
||||||
@@ -459,6 +469,8 @@ const liveConfigDefault = {
|
|||||||
},
|
},
|
||||||
thumbnails: {
|
thumbnails: {
|
||||||
media: 'clips' as const,
|
media: 'clips' as const,
|
||||||
|
size: '100px',
|
||||||
|
mode: 'none' as const,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -489,44 +501,77 @@ const jsmpegConfigSchema = z
|
|||||||
.optional();
|
.optional();
|
||||||
export type JSMPEGConfig = z.infer<typeof jsmpegConfigSchema>;
|
export type JSMPEGConfig = z.infer<typeof jsmpegConfigSchema>;
|
||||||
|
|
||||||
const liveNextPreviousControlConfigSchema = nextPreviousControlConfigSchema.merge(
|
const liveNextPreviousControlConfigSchema = nextPreviousControlConfigSchema.extend({
|
||||||
z.object({
|
// Live cannot show thumbnails, remove that option.
|
||||||
style: z
|
style: z.enum(['none', 'chevrons', 'icons']),
|
||||||
.enum(['none', 'chevrons', 'icons'])
|
});
|
||||||
.default(liveConfigDefault.controls.next_previous.style),
|
|
||||||
size: z.string().default(liveConfigDefault.controls.next_previous.size),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
export type LiveNextPreviousControlConfig = z.infer<
|
|
||||||
typeof liveNextPreviousControlConfigSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
const liveConfigSchema = z
|
const liveOverridableConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
provider: z.enum(LIVE_PROVIDERS).default(liveConfigDefault.provider),
|
provider: z.enum(LIVE_PROVIDERS).optional(),
|
||||||
preload: z.boolean().default(liveConfigDefault.preload),
|
|
||||||
webrtc: webrtcConfigSchema,
|
webrtc: webrtcConfigSchema,
|
||||||
jsmpeg: jsmpegConfigSchema,
|
jsmpeg: jsmpegConfigSchema,
|
||||||
lazy_load: z.boolean().default(liveConfigDefault.lazy_load),
|
|
||||||
draggable: z.boolean().default(liveConfigDefault.draggable),
|
|
||||||
controls: z
|
controls: z
|
||||||
.object({
|
.object({
|
||||||
next_previous: liveNextPreviousControlConfigSchema.default(
|
next_previous: liveNextPreviousControlConfigSchema.optional(),
|
||||||
liveConfigDefault.controls.next_previous,
|
|
||||||
),
|
|
||||||
thumbnails: thumbnailsControlSchema
|
thumbnails: thumbnailsControlSchema
|
||||||
.merge(
|
.merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
media: z.enum(['clips', 'snapshots']),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.merge(actionsSchema);
|
||||||
|
|
||||||
|
const liveConfigSchema = liveOverridableConfigSchema
|
||||||
|
.extend({
|
||||||
|
// Replace attributes from the overridable schema with those with defaults.
|
||||||
|
provider: liveOverridableConfigSchema.shape.provider.default(
|
||||||
|
liveConfigDefault.provider,
|
||||||
|
),
|
||||||
|
controls: z
|
||||||
|
.object({
|
||||||
|
next_previous: liveNextPreviousControlConfigSchema
|
||||||
|
.extend({
|
||||||
|
size: liveNextPreviousControlConfigSchema.shape.size.default(
|
||||||
|
liveConfigDefault.controls.next_previous.size,
|
||||||
|
),
|
||||||
|
style: liveNextPreviousControlConfigSchema.shape.style.default(
|
||||||
|
liveConfigDefault.controls.next_previous.style,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.default(liveConfigDefault.controls.next_previous),
|
||||||
|
thumbnails: thumbnailsControlSchema
|
||||||
|
.extend({
|
||||||
|
mode: thumbnailsControlSchema.shape.mode.default(
|
||||||
|
liveConfigDefault.controls.thumbnails.mode,
|
||||||
|
),
|
||||||
|
size: thumbnailsControlSchema.shape.size.default(
|
||||||
|
liveConfigDefault.controls.thumbnails.size,
|
||||||
|
),
|
||||||
media: z
|
media: z
|
||||||
.enum(['clips', 'snapshots'])
|
.enum(['clips', 'snapshots'])
|
||||||
.default(liveConfigDefault.controls.thumbnails.media),
|
.default(liveConfigDefault.controls.thumbnails.media),
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
.default(liveConfigDefault.controls.thumbnails),
|
.default(liveConfigDefault.controls.thumbnails),
|
||||||
})
|
})
|
||||||
.default(liveConfigDefault.controls),
|
.default(liveConfigDefault.controls),
|
||||||
|
|
||||||
|
// Non-overrideable parameters.
|
||||||
|
preload: z.boolean().default(liveConfigDefault.preload),
|
||||||
|
lazy_load: z.boolean().default(liveConfigDefault.lazy_load),
|
||||||
|
draggable: z.boolean().default(liveConfigDefault.draggable),
|
||||||
|
overrides: z
|
||||||
|
.object({
|
||||||
|
conditions: frigateCardConditionSchema,
|
||||||
|
overrides: liveOverridableConfigSchema,
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.merge(actionsSchema)
|
|
||||||
.default(liveConfigDefault);
|
.default(liveConfigDefault);
|
||||||
export type LiveConfig = z.infer<typeof liveConfigSchema>;
|
export type LiveConfig = z.infer<typeof liveConfigSchema>;
|
||||||
|
|
||||||
@@ -583,6 +628,7 @@ const viewerConfigDefault = {
|
|||||||
style: 'thumbnails' as const,
|
style: 'thumbnails' as const,
|
||||||
},
|
},
|
||||||
thumbnails: {
|
thumbnails: {
|
||||||
|
size: '100px',
|
||||||
mode: 'none' as const,
|
mode: 'none' as const,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -609,9 +655,16 @@ const viewerConfigSchema = z
|
|||||||
next_previous: viewerNextPreviousControlConfigSchema.default(
|
next_previous: viewerNextPreviousControlConfigSchema.default(
|
||||||
viewerConfigDefault.controls.next_previous,
|
viewerConfigDefault.controls.next_previous,
|
||||||
),
|
),
|
||||||
thumbnails: thumbnailsControlSchema.default(
|
thumbnails: thumbnailsControlSchema
|
||||||
viewerConfigDefault.controls.thumbnails,
|
.extend({
|
||||||
|
mode: thumbnailsControlSchema.shape.mode.default(
|
||||||
|
viewerConfigDefault.controls.thumbnails.mode,
|
||||||
),
|
),
|
||||||
|
size: thumbnailsControlSchema.shape.size.default(
|
||||||
|
viewerConfigDefault.controls.thumbnails.size,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.default(viewerConfigDefault.controls.thumbnails),
|
||||||
})
|
})
|
||||||
.default(viewerConfigDefault.controls),
|
.default(viewerConfigDefault.controls),
|
||||||
})
|
})
|
||||||
@@ -656,7 +709,7 @@ const dimensionsConfigSchema = z
|
|||||||
*/
|
*/
|
||||||
export const frigateCardConfigSchema = z.object({
|
export const frigateCardConfigSchema = z.object({
|
||||||
// Main configuration sections.
|
// Main configuration sections.
|
||||||
cameras: cameraConfigDefaultSchema.array().nonempty(),
|
cameras: cameraConfigSchema.array().nonempty(),
|
||||||
view: viewConfigSchema,
|
view: viewConfigSchema,
|
||||||
menu: menuConfigSchema,
|
menu: menuConfigSchema,
|
||||||
live: liveConfigSchema,
|
live: liveConfigSchema,
|
||||||
|
|||||||
Reference in New Issue
Block a user