Initial configuration reorganization.

This commit is contained in:
Dermot Duffy
2021-11-01 12:43:33 -07:00
parent 12cee94d49
commit e0daa17834
11 changed files with 638 additions and 477 deletions
+52 -56
View File
@@ -192,72 +192,72 @@ export class FrigateCard extends LitElement {
protected _getMenuButtons(): MenuButton[] {
const buttons: MenuButton[] = [];
if (this.config.menu_buttons?.frigate ?? true) {
if (this.config.menu.buttons.frigate) {
buttons.push({
type: 'internal-menu-icon',
tap_action: 'frigate',
title: localize('menu.frigate'),
title: localize('config.menu.buttons.frigate'),
});
}
if (this.config.menu_buttons?.live ?? true) {
if (this.config.menu.buttons.live) {
buttons.push({
type: 'internal-menu-icon',
tap_action: 'live',
title: localize('menu.live'),
title: localize('config.view.views.live'),
icon: 'mdi:cctv',
emphasize: this._view.is('live'),
});
}
if (this.config.menu_buttons?.clips ?? true) {
if (this.config.menu.buttons.clips) {
buttons.push({
type: 'internal-menu-icon',
tap_action: 'clips',
hold_action: 'clip',
title: localize('menu.clips'),
title: localize('config.view.views.clips'),
icon: 'mdi:filmstrip',
emphasize: this._view.is('clips'),
});
}
if (this.config.menu_buttons?.snapshots ?? true) {
if (this.config.menu.buttons.snapshots) {
buttons.push({
type: 'internal-menu-icon',
tap_action: 'snapshots',
hold_action: 'snapshot',
title: localize('menu.snapshots'),
title: localize('config.view.views.snapshots'),
icon: 'mdi:camera',
emphasize: this._view.is('snapshots'),
});
}
if (this.config.menu_buttons?.image ?? false) {
if (this.config.menu.buttons.image) {
buttons.push({
type: 'internal-menu-icon',
tap_action: 'image',
title: localize('menu.image'),
title: localize('config.view.views.image'),
icon: 'mdi:image',
emphasize: this._view.is('image'),
});
}
if (this._view.isViewerView() && (this.config.menu_buttons?.download ?? true)) {
if (this.config.menu.buttons.download && this._view.isViewerView()) {
buttons.push({
type: 'internal-menu-icon',
tap_action: 'download',
title: localize('menu.download'),
title: localize('config.menu.buttons.download'),
icon: 'mdi:download',
});
}
if ((this.config.menu_buttons?.frigate_ui ?? true) && this.config.frigate_url) {
if (this.config.menu.buttons.frigate_ui && this.config.frigate.url) {
buttons.push({
type: 'internal-menu-icon',
tap_action: 'frigate_ui',
title: localize('menu.frigate_ui'),
title: localize('config.menu.buttons.frigate_ui'),
icon: 'mdi:web',
});
}
if ((this.config.menu_buttons?.fullscreen ?? true) && screenfull.isEnabled) {
if (this.config.menu.buttons.fullscreen && screenfull.isEnabled) {
buttons.push({
type: 'internal-menu-icon',
tap_action: 'fullscreen',
title: localize('menu.fullscreen'),
title: localize('config.menu.buttons.fullscreen'),
icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
});
}
@@ -300,8 +300,8 @@ export class FrigateCard extends LitElement {
}
// Option 1: Name specified in config -> done!
if (this.config.frigate_camera_name) {
return this.config.frigate_camera_name;
if (this.config.frigate.camera_name) {
return this.config.frigate.camera_name;
}
if (this.config.camera_entity) {
@@ -436,7 +436,7 @@ export class FrigateCard extends LitElement {
this._message = null;
if (view === undefined) {
this._view = new View({ view: this.config.view_default });
this._view = new View({ view: this.config.view.default });
} else {
this._view = view;
}
@@ -505,7 +505,7 @@ export class FrigateCard extends LitElement {
}
const path =
`/api/frigate/${this.config.frigate_client_id}` +
`/api/frigate/${this.config.frigate.client_id}` +
`/notifications/${event_id}/` +
`${this._view.isClipRelatedView() ? 'clip.mp4' : 'snapshot.jpg'}` +
`?download=true`;
@@ -595,22 +595,22 @@ export class FrigateCard extends LitElement {
* @returns The URL or null if unavailable.
*/
protected _getFrigateURLFromContext(): string | null {
if (!this.config.frigate_url) {
if (!this.config.frigate.url) {
return null;
}
if (!this._frigateCameraName) {
return this.config.frigate_url;
return this.config.frigate.url;
} else if (this._view.is('live')) {
return `${this.config.frigate_url}/cameras/${this._frigateCameraName}`;
return `${this.config.frigate.url}/cameras/${this._frigateCameraName}`;
}
return `${this.config.frigate_url}/events?camera=${this._frigateCameraName}`;
return `${this.config.frigate.url}/events?camera=${this._frigateCameraName}`;
}
/**
* Handle interaction with the card.
*/
protected _interactionHandler(): void {
if (!this.config.view_timeout) {
if (!this.config.view.timeout) {
return;
}
if (this._interactionTimerID) {
@@ -619,7 +619,7 @@ export class FrigateCard extends LitElement {
this._interactionTimerID = window.setTimeout(() => {
this._interactionTimerID = null;
this._changeView();
}, this.config.view_timeout * 1000);
}, this.config.view.timeout * 1000);
}
/**
@@ -628,15 +628,14 @@ export class FrigateCard extends LitElement {
*/
protected _renderMenu(): TemplateResult | void {
const classes = {
'hover-menu': this.config.menu_mode.startsWith('hover-'),
'hover-menu': this.config.menu.mode.startsWith('hover-'),
};
return html`
<frigate-card-menu
class="${classMap(classes)}"
.hass=${this._hass}
.menuMode=${this.config.menu_mode}
.menuConfig=${this.config.menu}
.buttons=${this._getMenuButtons()}
.buttonSize=${this.config.menu_button_size}
class="${classMap(classes)}"
@frigate-card:menu-interaction=${this._menuInteractionHandler.bind(this)}
></frigate-card-menu>
`;
@@ -655,10 +654,10 @@ export class FrigateCard extends LitElement {
}
return {
mediaType: this._view.isClipRelatedView() ? 'clips' : 'snapshots',
clientId: this.config.frigate_client_id,
clientId: this.config.frigate.client_id,
cameraName: this._frigateCameraName,
label: this.config.label,
zone: this.config.zone,
label: this.config.frigate.label,
zone: this.config.frigate.zone,
};
}
@@ -760,7 +759,7 @@ export class FrigateCard extends LitElement {
* context.
*/
protected _isAspectRatioEnforced(): boolean {
const aspect_ratio_mode = this.config.dimensions?.aspect_ratio_mode ?? 'dynamic';
const aspectRatioMode = this.config.dimensions.aspect_ratio_mode;
// Do not artifically constrain aspect ratio if:
// - It's fullscreen.
@@ -769,8 +768,8 @@ export class FrigateCard extends LitElement {
return !(
(screenfull.isEnabled && screenfull.isFullscreen) ||
aspect_ratio_mode == 'unconstrained' ||
(aspect_ratio_mode == 'dynamic' && this._view.isMediaView())
aspectRatioMode == 'unconstrained' ||
(aspectRatioMode == 'dynamic' && this._view.isMediaView())
);
}
@@ -784,14 +783,14 @@ export class FrigateCard extends LitElement {
return null;
}
const aspect_ratio_mode = this.config.dimensions?.aspect_ratio_mode ?? 'dynamic';
if (aspect_ratio_mode == 'dynamic' && this._mediaShowInfo) {
const aspectRatioMode = this.config.dimensions.aspect_ratio_mode;
if (aspectRatioMode == 'dynamic' && this._mediaShowInfo) {
return (this._mediaShowInfo.height / this._mediaShowInfo.width) * 100;
}
const default_aspect_ratio = this.config.dimensions?.aspect_ratio;
if (default_aspect_ratio) {
return (default_aspect_ratio[1] / default_aspect_ratio[0]) * 100;
const defaultAspectRatio = this.config.dimensions.aspect_ratio;
if (defaultAspectRatio) {
return (defaultAspectRatio[1] / defaultAspectRatio[0]) * 100;
} else {
return (9 / 16) * 100;
}
@@ -836,7 +835,7 @@ export class FrigateCard extends LitElement {
window.innerWidth / window.innerHeight
) {
// If the menu is outside the media (i.e. above/below) allow space for it.
const allowance = ['above', 'below'].includes(this.config.menu_mode)
const allowance = ['above', 'below'].includes(this.config.menu.mode)
? MENU_HEIGHT
: 0;
innerStyle['max-width'] = `calc(${
@@ -850,7 +849,7 @@ export class FrigateCard extends LitElement {
};
return html` <ha-card @click=${this._interactionHandler}>
${this.config.menu_mode == 'above' ? this._renderMenu() : ''}
${this.config.menu.mode == 'above' ? this._renderMenu() : ''}
<div class="container outer" style="${styleMap(outerStyle)}">
<div class="${classMap(contentClasses)}" style="${styleMap(innerStyle)}">
${this._frigateCameraName == undefined
@@ -864,7 +863,7 @@ export class FrigateCard extends LitElement {
: this._render()}
</div>
</div>
${this.config.menu_mode != 'above' ? this._renderMenu() : ''}
${this.config.menu.mode != 'above' ? this._renderMenu() : ''}
</ha-card>`;
}
@@ -891,16 +890,16 @@ export class FrigateCard extends LitElement {
gallery: this._view.isGalleryView(),
};
const galleryClasses = {
hidden: this.config.live_preload && !this._view.isGalleryView(),
hidden: this.config.live.preload && !this._view.isGalleryView(),
};
const viewerClasses = {
hidden: this.config.live_preload && !this._view.isViewerView(),
hidden: this.config.live.preload && !this._view.isViewerView(),
};
const liveClasses = {
hidden: this.config.live_preload && this._view.view != 'live',
hidden: this.config.live.preload && this._view.view != 'live',
};
const imageClasses = {
hidden: this.config.live_preload && this._view.view != 'image',
hidden: this.config.live.preload && this._view.view != 'image',
};
return html`
@@ -908,7 +907,7 @@ export class FrigateCard extends LitElement {
${this._message ? renderMessage(this._message) : ``}
${!this._message && this._view.is('image')
? html` <frigate-card-image
.image=${this.config.image}
.imageConfig=${this.config.image}
class="${classMap(imageClasses)}"
@frigate-card:media-show=${this._mediaShowHandler}
@frigate-card:message=${this._messageHandler}
@@ -931,11 +930,8 @@ export class FrigateCard extends LitElement {
.hass=${this._hass}
.view=${this._view}
.browseMediaQueryParameters=${mediaQueryParameters}
.nextPreviousControlStyle=${this.config.controls?.nextprev ?? 'thumbnails'}
.nextPreviousControlSize=${this.config.controls?.nextprev_size}
.autoplayClip=${this.config.autoplay_clip}
.viewerConfig=${this.config.event_viewer}
.resolvedMediaCache=${this._resolvedMediaCache}
.lazyLoad=${this.config.event_viewer?.lazy_load ?? true}
class="${classMap(viewerClasses)}"
@frigate-card:change-view=${this._changeViewHandler}
@frigate-card:media-show=${this._mediaShowHandler}
@@ -947,14 +943,14 @@ export class FrigateCard extends LitElement {
: ``}
${
// Note the subtle difference in condition below vs the other views in order
// to always render the live view for live_preload mode.
(!this._message && this._view.is('live')) || this.config.live_preload
// to always render the live view for live.preload mode.
(!this._message && this._view.is('live')) || this.config.live.preload
? html`
<frigate-card-live
.hass=${this._hass}
.config=${this.config}
.frigateCameraName=${this._frigateCameraName}
.preload=${this.config.live_preload && !this._view.is('live')}
.preload=${this.config.live.preload && !this._view.is('live')}
class="${classMap(liveClasses)}"
@frigate-card:media-show=${this._mediaShowHandler}
@frigate-card:pause=${this._pauseHandler}
+2 -2
View File
@@ -214,8 +214,8 @@ export function dispatchErrorMessageEvent(element: HTMLElement, message: string)
* @returns A boolean indicating whether or not to allow an update.
*/
export function shouldUpdateBasedOnHass(
newHass: HomeAssistant | null,
oldHass: HomeAssistant | undefined,
newHass: HomeAssistant | undefined | null,
oldHass: HomeAssistant | undefined | null,
entities: string[] | null,
): boolean {
if (!newHass || !entities) {
+5 -3
View File
@@ -3,17 +3,19 @@ import { customElement, property } from 'lit/decorators.js';
import { dispatchMediaShowEvent } from '../common.js';
import imageStyle from '../scss/image.scss';
import type { ImageViewConfig } from '../types.js';
import defaultImage from '../images/frigate-bird-in-sky.jpg'
import imageStyle from '../scss/image.scss';
@customElement('frigate-card-image')
export class FrigateCardImage extends LitElement {
@property({ attribute: false })
protected image?: string;
protected imageConfig?: ImageViewConfig;
protected render(): TemplateResult | void {
return html` <img
src=${this.image || defaultImage}
src=${this.imageConfig?.src || defaultImage}
@load=${(e) => {
dispatchMediaShowEvent(this, e);
}}
+6 -6
View File
@@ -1,5 +1,5 @@
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import type { ExtendedHomeAssistant, FrigateCardConfig, MediaShowInfo } from '../types.js';
import type { ExtendedHomeAssistant, FrigateCardConfig, MediaShowInfo, WebRTCConfig } from '../types.js';
import { HomeAssistant } from 'custom-card-helpers';
import { customElement, property } from 'lit/decorators.js';
import { until } from 'lit/directives/until.js';
@@ -75,24 +75,24 @@ export class FrigateCardLive extends LitElement {
return;
}
return html` ${this.config.live_provider == 'frigate'
return html` ${this.config.live.provider == 'frigate'
? html` <frigate-card-live-frigate
.hass=${this.hass}
.cameraEntity=${this.config.camera_entity}
@frigate-card:media-show=${this._mediaShowHandler}
>
</frigate-card-live-frigate>`
: this.config.live_provider == 'webrtc'
: this.config.live.provider == 'webrtc'
? html`<frigate-card-live-webrtc
.hass=${this.hass}
.webRTCConfig=${this.config.webrtc || {}}
.webRTCConfig=${this.config.live.webrtc || {}}
@frigate-card:media-show=${this._mediaShowHandler}
>
</frigate-card-live-webrtc>`
: html` <frigate-card-live-jsmpeg
.hass=${this.hass}
.cameraName=${this.frigateCameraName}
.clientId=${this.config.frigate_client_id}
.clientId=${this.config.frigate.client_id}
@frigate-card:media-show=${this._mediaShowHandler}
>
</frigate-card-live-jsmpeg>`}`;
@@ -152,7 +152,7 @@ export class FrigateCardLiveFrigate extends LitElement {
@customElement('frigate-card-live-webrtc')
export class FrigateCardLiveWebRTC extends LitElement {
@property({ attribute: false })
protected webRTCConfig?: Record<string, unknown>;
protected webRTCConfig?: WebRTCConfig;
protected hass?: HomeAssistant & ExtendedHomeAssistant;
protected _webRTCElement: HTMLElement | null = null;
+37 -32
View File
@@ -16,8 +16,8 @@ import { actionHandler } from '../action-handler-directive.js';
import type {
ExtendedHomeAssistant,
FrigateMenuMode,
MenuButton,
MenuConfig,
MenuInteraction,
} from '../types.js';
import { dispatchFrigateCardEvent, shouldUpdateBasedOnHass } from '../common.js';
@@ -30,10 +30,16 @@ export const MENU_HEIGHT = 46;
@customElement('frigate-card-menu')
export class FrigateCardMenu extends LitElement {
@property({ attribute: false })
public hass!: HomeAssistant & ExtendedHomeAssistant;
public hass?: HomeAssistant & ExtendedHomeAssistant;
@property({ attribute: false })
protected menuMode: FrigateMenuMode = 'hidden-top';
set menuConfig(menuConfig: MenuConfig) {
this._menuConfig = menuConfig;
if (menuConfig) {
this.style.setProperty('--frigate-card-menu-button-size', menuConfig.button_size);
}
}
public _menuConfig?: MenuConfig;
@property({ attribute: false })
protected expand = false;
@@ -41,13 +47,8 @@ export class FrigateCardMenu extends LitElement {
@property({ attribute: false })
public buttons: MenuButton[] = [];
@property({ attribute: false })
set buttonSize(buttonSize: string) {
this.style.setProperty('--frigate-card-menu-button-size', buttonSize);
}
protected _interactionHandler(ev: CustomEvent, button: MenuButton): void {
if (this.menuMode.startsWith('hidden-')) {
if (this._menuConfig?.mode.startsWith('hidden-')) {
if (button.type == 'internal-menu-icon' && button.tap_action === 'frigate') {
this.expand = !this.expand;
return;
@@ -82,7 +83,7 @@ export class FrigateCardMenu extends LitElement {
}
// Render a menu button.
protected _renderButton(button: MenuButton): TemplateResult {
protected _renderButton(button: MenuButton): TemplateResult | void {
let state: HassEntity | null = null;
let emphasize = false;
let title = button.title;
@@ -90,6 +91,9 @@ export class FrigateCardMenu extends LitElement {
const style = ('style' in button ? button.style : {}) || {};
if (button.type === 'custom:frigate-card-menu-state-icon') {
if (!this.hass) {
return;
}
state = this.hass.states[button.entity];
emphasize =
!!state && button.state_color && ['on', 'active', 'home'].includes(state.state);
@@ -133,9 +137,9 @@ export class FrigateCardMenu extends LitElement {
}
// Render the Frigate menu button.
protected _renderFrigateButton(button: MenuButton): TemplateResult {
protected _renderFrigateButton(button: MenuButton): TemplateResult | void {
const icon =
this.menuMode.startsWith('hidden-') && !this.expand
this._menuConfig?.mode.startsWith('hidden-') && !this.expand
? 'mdi:alpha-f-box-outline'
: 'mdi:alpha-f-box';
@@ -143,7 +147,12 @@ export class FrigateCardMenu extends LitElement {
}
// Render the menu.
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (!this._menuConfig) {
return;
}
const mode = this._menuConfig.mode;
const isFrigateButton = function (button: MenuButton): boolean {
return button.type === 'internal-menu-icon' && button.tap_action === 'frigate';
};
@@ -151,33 +160,29 @@ export class FrigateCardMenu extends LitElement {
// If the menu is off, or if it's in hidden mode but there's no button to
// unhide it, just show nothing.
if (
this.menuMode == 'none' ||
(this.menuMode.startsWith('hidden-') && !this.buttons.find(isFrigateButton))
mode == 'none' ||
(mode.startsWith('hidden-') && !this.buttons.find(isFrigateButton))
) {
return html``;
return;
}
const classes = {
'frigate-card-menu': true,
'overlay-hidden':
this.menuMode.startsWith('hidden-') ||
this.menuMode.startsWith('overlay-') ||
this.menuMode.startsWith('hover-'),
mode.startsWith('hidden-') ||
mode.startsWith('overlay-') ||
mode.startsWith('hover-'),
'expanded-horizontal':
(this.menuMode.startsWith('overlay-') ||
this.menuMode.startsWith('hover-') ||
this.expand) &&
(this.menuMode.endsWith('-top') || this.menuMode.endsWith('-bottom')),
(mode.startsWith('overlay-') || mode.startsWith('hover-') || this.expand) &&
(mode.endsWith('-top') || mode.endsWith('-bottom')),
'expanded-vertical':
(this.menuMode.startsWith('overlay-') ||
this.menuMode.startsWith('hover-') ||
this.expand) &&
(this.menuMode.endsWith('-left') || this.menuMode.endsWith('-right')),
full: ['above', 'below'].includes(this.menuMode),
left: this.menuMode.endsWith('-left'),
right: this.menuMode.endsWith('-right'),
top: this.menuMode.endsWith('-top'),
bottom: this.menuMode.endsWith('-bottom'),
(mode.startsWith('overlay-') || mode.startsWith('hover-') || this.expand) &&
(mode.endsWith('-left') || mode.endsWith('-right')),
full: mode == 'above' || mode == 'below',
left: mode.endsWith('-left'),
right: mode.endsWith('-right'),
top: mode.endsWith('-top'),
bottom: mode.endsWith('-bottom'),
};
return html`
+14 -15
View File
@@ -2,30 +2,29 @@ import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { NextPreviousControlStyle } from '../types.js';
import { NextPreviousControlConfig } from '../types.js';
import controlStyle from '../scss/next-previous-control.scss';
@customElement('frigate-card-next-previous-control')
export class FrigateCardMessage extends LitElement {
export class FrigateCardNextPreviousControl extends LitElement {
@property({ attribute: false })
protected direction?: 'next' | 'previous';
@property({ attribute: false })
protected controlStyle?: NextPreviousControlStyle;
set controlConfig(controlConfig: NextPreviousControlConfig | undefined) {
if (controlConfig?.size) {
this.style.setProperty('--frigate-card-next-prev-size', controlConfig.size);
}
this._controlConfig = controlConfig;
}
protected _controlConfig?: NextPreviousControlConfig;
@property({ attribute: false })
protected thumbnail?: string;
@property({ attribute: false })
set controlSize(controlSize: string | undefined) {
if (controlSize) {
this.style.setProperty('--frigate-card-next-prev-size', controlSize);
}
}
protected render(): TemplateResult {
if (!this.controlStyle || this.controlStyle == 'none') {
if (!this._controlConfig || this._controlConfig.style == 'none') {
return html``;
}
@@ -33,12 +32,12 @@ export class FrigateCardMessage extends LitElement {
controls: true,
previous: this.direction == 'previous',
next: this.direction == 'next',
thumbnails: this.controlStyle == 'thumbnails',
chevrons: this.controlStyle == 'chevrons',
button: this.controlStyle == 'chevrons',
thumbnails: this._controlConfig.style == 'thumbnails',
chevrons: this._controlConfig.style == 'chevrons',
button: this._controlConfig.style == 'chevrons',
};
if (this.controlStyle == 'chevrons') {
if (this._controlConfig.style == 'chevrons') {
const icon = this.direction == 'previous' ? 'mdi:chevron-left' : 'mdi:chevron-right';
// TODO: Upon a safe distance from the release of HA 2021.11 these
+39 -55
View File
@@ -19,7 +19,7 @@ import type {
BrowseMediaSource,
ExtendedHomeAssistant,
MediaShowInfo,
NextPreviousControlStyle,
ViewerConfig,
} from '../types.js';
import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js';
import { View } from '../view.js';
@@ -49,24 +49,15 @@ export class FrigateCardViewer extends LitElement {
@property({ attribute: false })
protected view?: View;
@property({ attribute: false })
protected viewerConfig?: ViewerConfig;
@property({ attribute: false })
protected browseMediaQueryParameters?: BrowseMediaQueryParameters;
@property({ attribute: false })
protected nextPreviousControlStyle?: NextPreviousControlStyle;
@property({ attribute: false })
protected nextPreviousControlSize?: string;
@property({ attribute: false })
protected autoplayClip?: boolean;
@property({ attribute: false })
protected resolvedMediaCache?: ResolvedMediaCache;
@property({ attribute: false })
protected lazyLoad?: boolean;
/**
* Resolve all the given media for a target.
* @param target The target to resolve media from.
@@ -92,21 +83,23 @@ export class FrigateCardViewer extends LitElement {
return errorFree;
}
/**
* Master render method.
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
return html`${until(this._render(), renderProgressIndicator())}`;
}
/**
* Asyncronously render the element.
* @returns A template to render.
* @returns A rendered template.
*/
protected async _render(): Promise<TemplateResult | void> {
if (!this.hass || !this.view || !this.browseMediaQueryParameters) {
return html``;
}
let autoplay = true;
if (this.view.is('clip') || this.view.is('snapshot')) {
let parent: BrowseMediaSource | null = null;
try {
@@ -129,13 +122,6 @@ export class FrigateCardViewer extends LitElement {
}
this.view.target = parent;
this.view.childIndex = childIndex;
// In this block, no clip has been manually selected, so this is loading
// the most recent clip on card load. In this mode, autoplay of the clip
// may be disabled by configuration. If does not make sense to disable
// autoplay when the user has explicitly picked an event to play in the
// gallery.
autoplay = this.autoplayClip ?? true;
}
if (this.view.target && !(await this._resolveAllMediaForTarget(this.view.target))) {
@@ -144,13 +130,10 @@ export class FrigateCardViewer extends LitElement {
return html` <frigate-card-viewer-core
.view=${this.view}
.nextPreviousControlStyle=${this.nextPreviousControlStyle}
.nextPreviousControlSize=${this.nextPreviousControlSize}
.viewerConfig=${this.viewerConfig}
.resolvedMediaCache=${this.resolvedMediaCache}
.autoplayClip=${autoplay}
.hass=${this.hass}
.browseMediaQueryParameters=${this.browseMediaQueryParameters}
.lazyLoad=${this.lazyLoad}
>
</frigate-card-viewer-core>`;
}
@@ -165,29 +148,20 @@ export class FrigateCardViewer extends LitElement {
@customElement('frigate-card-viewer-core')
export class FrigateCardViewerCore extends LitElement {
@property({ attribute: false })
protected hass?: HomeAssistant & ExtendedHomeAssistant;
@property({ attribute: false })
protected view?: View;
@property({ attribute: false })
protected nextPreviousControlStyle?: NextPreviousControlStyle;
@property({ attribute: false })
protected nextPreviousControlSize?: string;
@property({ attribute: false })
protected resolvedMediaCache?: ResolvedMediaCache;
@property({ attribute: false })
protected autoplayClip?: boolean;
@property({ attribute: false })
protected hass?: HomeAssistant & ExtendedHomeAssistant;
protected viewerConfig?: ViewerConfig;
@property({ attribute: false })
protected browseMediaQueryParameters?: BrowseMediaQueryParameters;
@property({ attribute: false })
protected lazyLoad?: boolean;
protected resolvedMediaCache?: ResolvedMediaCache;
// Media carousel object.
protected _carousel?: EmblaCarouselType;
@@ -418,7 +392,7 @@ export class FrigateCardViewerCore extends LitElement {
* Lazily load media in the carousel.
*/
protected _lazyLoadMediaHandler(): void {
if (!this.lazyLoad || !this._carousel) {
if (!this.viewerConfig?.lazy_load || !this._carousel) {
return;
}
const slides = this._carousel.slideNodes();
@@ -504,8 +478,7 @@ export class FrigateCardViewerCore extends LitElement {
${neighbors && neighbors.previous
? html`<frigate-card-next-previous-control
.direction=${'previous'}
.controlStyle=${this.nextPreviousControlStyle}
.controlSize=${this.nextPreviousControlSize}
.controlConfig=${this.viewerConfig?.controls.next_previous}
.thumbnail=${neighbors.previous.thumbnail}
.title=${neighbors.previous.title}
@click=${() => this._nextPreviousHandler('previous')}
@@ -519,8 +492,7 @@ export class FrigateCardViewerCore extends LitElement {
${neighbors && neighbors.next
? html`<frigate-card-next-previous-control
.direction=${'next'}
.controlStyle=${this.nextPreviousControlStyle}
.controlSize=${this.nextPreviousControlSize}
.controlConfig=${this.viewerConfig?.controls.next_previous}
.thumbnail=${neighbors.next.thumbnail}
.title=${neighbors.next.title}
@click=${() => this._nextPreviousHandler('next')}
@@ -594,7 +566,7 @@ export class FrigateCardViewerCore extends LitElement {
): TemplateResult | void {
// media that can be expanded (folders) cannot be resolved to a single media
// item, skip them.
if (!this.view || !BrowseMediaUtil.isTrueMedia(mediaToRender)) {
if (!this.view || !this.viewerConfig || !BrowseMediaUtil.isTrueMedia(mediaToRender)) {
return;
}
@@ -603,20 +575,32 @@ export class FrigateCardViewerCore extends LitElement {
return;
}
// In this block, no clip has been manually selected, so this is loading
// the most recent clip on card load. In this mode, autoplay of the clip
// may be disabled by configuration. If does not make sense to disable
// autoplay when the user has explicitly picked an event to play in the
// gallery.
let autoplay = true;
if (this.view.is('clip') || this.view.is('snapshot')) {
autoplay = this.viewerConfig.autoplay_clip;
}
const lazyLoad = this.viewerConfig.lazy_load;
return html`
<div class="embla__slide">
${this.view.isClipRelatedView()
? resolvedMedia?.mime_type.toLowerCase() == 'application/x-mpegurl'
? html`<frigate-card-ha-hls-player
.hass=${this.hass}
url=${ifDefined(this.lazyLoad ? undefined : resolvedMedia.url)}
data-url=${ifDefined(this.lazyLoad ? resolvedMedia.url : undefined)}
url=${ifDefined(lazyLoad ? undefined : resolvedMedia.url)}
data-url=${ifDefined(lazyLoad ? resolvedMedia.url : undefined)}
title="${mediaToRender.title}"
muted
controls
playsinline
allow-exoplayer
?autoplay="${this.autoplayClip}"
?autoplay="${autoplay}"
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) =>
this._mediaShowEventHandler(slideIndex, e)}
>
@@ -626,7 +610,7 @@ export class FrigateCardViewerCore extends LitElement {
muted
controls
playsinline
?autoplay="${this.autoplayClip}"
?autoplay="${autoplay}"
@loadedmetadata="${(e: Event) => {
this._mediaShowInfoHandler(slideIndex, createMediaShowInfo(e));
}}"
@@ -634,14 +618,14 @@ export class FrigateCardViewerCore extends LitElement {
@pause=${() => dispatchPauseEvent(this)}
>
<source
src=${ifDefined(this.lazyLoad ? undefined : resolvedMedia.url)}
data-src=${ifDefined(this.lazyLoad ? resolvedMedia.url : undefined)}
src=${ifDefined(lazyLoad ? undefined : resolvedMedia.url)}
data-src=${ifDefined(lazyLoad ? resolvedMedia.url : undefined)}
type="${resolvedMedia.mime_type}"
/>
</video>`
: html`<img
src=${ifDefined(this.lazyLoad ? IMG_EMPTY : resolvedMedia.url)}
data-src=${ifDefined(this.lazyLoad ? resolvedMedia.url : undefined)}
src=${ifDefined(lazyLoad ? IMG_EMPTY : resolvedMedia.url)}
data-src=${ifDefined(lazyLoad ? resolvedMedia.url : undefined)}
title="${mediaToRender.title}"
@click=${() => {
if (this._carousel?.clickAllowed()) {
+168 -139
View File
@@ -1,10 +1,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// TODO Add editor for control sizes
// TODO reorganizse editor by section
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers';
import { localize } from './localize/localize.js';
import type { FrigateCardConfig } from './types.js';
import {
FrigateCardConfig,
frigateCardConfigDefaults,
frigateCardConfigSchema,
} from './types.js';
import frigate_card_editor_style from './scss/editor.scss';
@@ -84,66 +92,70 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
return html``;
}
// The climate more-info has ha-switch and paper-dropdown-menu elements that are lazy loaded unless explicitly done here
// The climate more-info has ha-switch and paper-dropdown-menu elements that
// are lazy loaded unless explicitly loaded via climate here.
this._helpers.importMoreInfoControl('climate');
// You can restrict on domain type
const cameraEntities = this._getEntities('camera');
const webrtcCameraEntity =
this._config?.webrtc && (this._config?.webrtc as any).entity
? (this._config?.webrtc as any).entity
this._config?.live?.webrtc && (this._config?.live.webrtc as any).entity
? (this._config?.live.webrtc as any).entity
: '';
const viewModes = {
'': '',
live: localize('menu.live'),
clips: localize('menu.clips'),
snapshots: localize('menu.snapshots'),
clip: localize('menu.clip'),
snapshot: localize('menu.snapshot'),
image: localize('menu.image'),
live: localize('config.view.views.live'),
clips: localize('config.view.views.clips'),
snapshots: localize('config.view.views.snapshots'),
clip: localize('config.view.views.clip'),
snapshot: localize('config.view.views.snapshot'),
image: localize('config.view.views.image'),
};
const menuModes = {
'': '',
none: localize('menu_mode.none'),
'hidden-top': localize('menu_mode.hidden-top'),
'hidden-left': localize('menu_mode.hidden-left'),
'hidden-bottom': localize('menu_mode.hidden-bottom'),
'hidden-right': localize('menu_mode.hidden-right'),
'overlay-top': localize('menu_mode.overlay-top'),
'overlay-left': localize('menu_mode.overlay-left'),
'overlay-bottom': localize('menu_mode.overlay-bottom'),
'overlay-right': localize('menu_mode.overlay-right'),
'hover-top': localize('menu_mode.hover-top'),
'hover-left': localize('menu_mode.hover-left'),
'hover-bottom': localize('menu_mode.hover-bottom'),
'hover-right': localize('menu_mode.hover-right'),
above: localize('menu_mode.above'),
below: localize('menu_mode.below'),
none: localize('config.menu.modes.none'),
'hidden-top': localize('config.menu.modes.hidden-top'),
'hidden-left': localize('config.menu.modes.hidden-left'),
'hidden-bottom': localize('config.menu.modes.hidden-bottom'),
'hidden-right': localize('config.menu.modes.hidden-right'),
'overlay-top': localize('config.menu.modes.overlay-top'),
'overlay-left': localize('config.menu.modes.overlay-left'),
'overlay-bottom': localize('config.menu.modes.overlay-bottom'),
'overlay-right': localize('config.menu.modes.overlay-right'),
'hover-top': localize('config.menu.modes.hover-top'),
'hover-left': localize('config.menu.modes.hover-left'),
'hover-bottom': localize('config.menu.modes.hover-bottom'),
'hover-right': localize('config.menu.modes.hover-right'),
above: localize('config.menu.modes.above'),
below: localize('config.menu.modes.below'),
};
const liveProvider = {
const liveProviders = {
'': '',
frigate: localize('live_provider.frigate'),
'frigate-jsmpeg': localize('live_provider.frigate-jsmpeg'),
webrtc: localize('live_provider.webrtc'),
frigate: localize('config.live.providers.frigate'),
'frigate-jsmpeg': localize('config.live.providers.frigate-jsmpeg'),
webrtc: localize('config.live.providers.webrtc'),
};
const controlsNextPrev = {
const eventViewerNextPreviousControlStyles = {
'': '',
thumbnails: localize('control.thumbnails'),
chevrons: localize('control.chevrons'),
none: localize('control.none'),
thumbnails: localize(
'config.event_viewer.controls.next_previous.styles.thumbnails',
),
chevrons: localize('config.event_viewer.controls.next_previous.styles.chevrons'),
none: localize('config.event_viewer.controls.next_previous.styles.none'),
};
const aspectRatioModes = {
'': '',
dynamic: localize('aspect_ratio_mode.dynamic'),
static: localize('aspect_ratio_mode.static'),
unconstrained: localize('aspect_ratio_mode.unconstrained'),
}
dynamic: localize('config.dimensions.aspect_ratio_modes.dynamic'),
static: localize('config.dimensions.aspect_ratio_modes.static'),
unconstrained: localize('config.dimensions.aspect_ratio_modes.unconstrained'),
};
const defaults = frigateCardConfigDefaults;
return html`
<div class="card-config">
@@ -158,7 +170,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
? html`
<div class="values">
<paper-dropdown-menu
label=${localize('editor.camera_entity')}
label=${localize('config.camera_entity')}
@value-changed=${this._valueChanged}
.configValue=${'camera_entity'}
>
@@ -174,20 +186,20 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</paper-listbox>
</paper-dropdown-menu>
<paper-input
label=${localize('editor.frigate_camera_name')}
.value=${this._config?.frigate_camera_name || ''}
.configValue=${'frigate_camera_name'}
label=${localize('config.frigate.camera_name')}
.value=${this._config?.frigate?.camera_name || ''}
.configValue=${'frigate.camera_name'}
@value-changed=${this._valueChanged}
></paper-input>
<paper-dropdown-menu
label=${localize('editor.default_view')}
label=${localize('config.view.default')}
@value-changed=${this._valueChanged}
.configValue=${'view_default'}
.configValue=${'view.default'}
>
<paper-listbox
slot="dropdown-content"
.selected=${Object.keys(viewModes).indexOf(
this._config?.view_default || '',
this._config?.view?.default || '',
)}
>
${Object.keys(viewModes).map((key) => {
@@ -210,62 +222,64 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${options.optional.show
? html` <div class="values">
<paper-dropdown-menu
.label=${localize('editor.live_provider')}
.label=${localize('config.live.provider')}
@value-changed=${this._valueChanged}
.configValue=${'live_provider'}
.configValue=${'live.provider'}
>
<paper-listbox
slot="dropdown-content"
.selected=${Object.keys(liveProvider).indexOf(
this._config?.live_provider || '',
.selected=${Object.keys(liveProviders).indexOf(
this._config?.live?.provider || '',
)}
>
${Object.keys(liveProvider).map((key) => {
${Object.keys(liveProviders).map((key) => {
return html`
<paper-item .label="${key}">${liveProvider[key]} </paper-item>
<paper-item .label="${key}">${liveProviders[key]} </paper-item>
`;
})}
</paper-listbox>
</paper-dropdown-menu>
<paper-input
label=${localize('editor.frigate_client_id')}
.value=${this._config?.frigate_client_id || ''}
.configValue=${'frigate_client_id'}
label=${localize('config.frigate.client_id')}
.value=${this._config?.frigate?.client_id || ''}
.configValue=${'frigate.client_id'}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
label=${localize('editor.view_timeout')}
label=${localize('config.view.timeout')}
prevent-invalid-input
allowed-pattern="[0-9]"
.value=${this._config?.view_timeout
? String(this._config.view_timeout)
.value=${this._config?.view?.timeout
? String(this._config?.view?.timeout)
: ''}
.configValue=${'view_timeout'}
.configValue=${'view.timeout'}
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
label=${localize('editor.frigate_url')}
.value=${this._config?.frigate_url || ''}
.configValue=${'frigate_url'}
label=${localize('config.frigate.url')}
.value=${this._config?.frigate?.url || ''}
.configValue=${'frigate.url'}
@value-changed=${this._valueChanged}
></paper-input>
<ha-formfield .label=${localize('editor.autoplay_clip')}>
<ha-formfield .label=${localize('config.event_viewer.autoplay_clip')}>
<ha-switch
.checked=${this._config?.autoplay_clip === true}
.configValue=${'autoplay_clip'}
.checked=${this._config?.event_viewer?.autoplay_clip ??
defaults.event_viewer.autoplay_clip}
.configValue=${'event_viewer.autoplay_clip'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
<ha-formfield .label=${localize('editor.live_preload')}>
<ha-formfield .label=${localize('config.live.preload')}>
<ha-switch
.checked=${this._config?.live_preload === true}
.configValue=${'live_preload'}
.checked=${this._config?.live?.preload ?? defaults.live.preload}
.configValue=${'live.preload'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
<ha-formfield .label=${localize('editor.lazy_load')}>
<ha-formfield .label=${localize('config.event_viewer.lazy_load')}>
<ha-switch
.checked=${this._config?.event_viewer?.lazy_load !== false}
.checked=${this._config?.event_viewer?.lazy_load ??
defaults.event_viewer.lazy_load}
.configValue=${'event_viewer.lazy_load'}
@change=${this._valueChanged}
></ha-switch>
@@ -281,14 +295,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</div>
${options.appearance.show
? html`<paper-dropdown-menu
.label=${localize('editor.menu_mode')}
.label=${localize('config.menu.mode')}
@value-changed=${this._valueChanged}
.configValue=${'menu_mode'}
.configValue=${'menu.mode'}
>
<paper-listbox
slot="dropdown-content"
.selected=${Object.keys(menuModes).indexOf(
this._config?.menu_mode || '',
this._config?.menu?.mode || '',
)}
>
${Object.keys(menuModes).map((key) => {
@@ -300,26 +314,28 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</paper-dropdown-menu>
<br />
<paper-dropdown-menu
.label=${localize('control.nextprev')}
.label=${localize('config.event_viewer.controls.next_previous.style')}
@value-changed=${this._valueChanged}
.configValue=${'controls.nextprev'}
.configValue=${'event_viewer.controls.next_previous.style'}
>
<paper-listbox
slot="dropdown-content"
.selected=${Object.keys(controlsNextPrev).indexOf(
this._config?.controls?.nextprev || '',
.selected=${Object.keys(eventViewerNextPreviousControlStyles).indexOf(
this._config?.event_viewer?.controls?.next_previous?.style || '',
)}
>
${Object.keys(controlsNextPrev).map((key) => {
${Object.keys(eventViewerNextPreviousControlStyles).map((key) => {
return html`
<paper-item .label="${key}"> ${controlsNextPrev[key]} </paper-item>
<paper-item .label="${key}">
${eventViewerNextPreviousControlStyles[key]}
</paper-item>
`;
})}
</paper-listbox>
</paper-dropdown-menu>
<br />
<paper-dropdown-menu
.label=${localize('dimensions.aspect_ratio_mode')}
.label=${localize('config.dimensions.aspect_ratio_mode')}
@value-changed=${this._valueChanged}
.configValue=${'dimensions.aspect_ratio_mode'}
>
@@ -338,7 +354,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</paper-dropdown-menu>
<br />
<paper-input
label=${localize('dimensions.aspect_ratio')}
label=${localize('config.dimensions.aspect_ratio')}
prevent-invalid-input
.value=${this._config?.dimensions?.aspect_ratio
? String(this._config?.dimensions?.aspect_ratio)
@@ -347,42 +363,23 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
@value-changed=${this._valueChanged}
></paper-input>
<paper-input
label=${localize('editor.image')}
label=${localize('config.image.src')}
prevent-invalid-input
.value=${this._config?.image
? String(this._config?.image)
.value=${this._config?.image?.src
? String(this._config?.image?.src)
: ''}
.configValue=${'image'}
.configValue=${'image.src'}
@value-changed=${this._valueChanged}
></paper-input>
<ha-formfield
.label=${localize('editor.show_button') +
': ' +
localize('menu.frigate')}
localize('config.menu.buttons.frigate')}
>
<ha-switch
.checked=${this._config?.menu_buttons?.frigate ?? true}
.configValue=${'menu_buttons.frigate'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
<br />
<ha-formfield
.label=${localize('editor.show_button') + ': ' + localize('menu.live')}
>
<ha-switch
.checked=${this._config?.menu_buttons?.live ?? true}
.configValue=${'menu_buttons.live'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
<br />
<ha-formfield
.label=${localize('editor.show_button') + ': ' + localize('menu.clips')}
>
<ha-switch
.checked=${this._config?.menu_buttons?.clips ?? true}
.configValue=${'menu_buttons.clips'}
.checked=${this._config?.menu?.buttons?.frigate ??
defaults.menu.buttons.frigate}
.configValue=${'menu.buttons.frigate'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
@@ -390,11 +387,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<ha-formfield
.label=${localize('editor.show_button') +
': ' +
localize('menu.snapshots')}
localize('config.view.views.live')}
>
<ha-switch
.checked=${this._config?.menu_buttons?.snapshots ?? true}
.configValue=${'menu_buttons.snapshots'}
.checked=${this._config?.menu?.buttons?.live ??
defaults.menu.buttons.live}
.configValue=${'menu.buttons.live'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
@@ -402,11 +400,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<ha-formfield
.label=${localize('editor.show_button') +
': ' +
localize('menu.image')}
localize('config.view.views.clips')}
>
<ha-switch
.checked=${this._config?.menu_buttons?.image ?? false}
.configValue=${'menu_buttons.image'}
.checked=${this._config?.menu?.buttons?.clips ??
defaults.menu.buttons.clips}
.configValue=${'menu.buttons.clips'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
@@ -414,11 +413,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<ha-formfield
.label=${localize('editor.show_button') +
': ' +
localize('menu.download')}
localize('config.view.views.snapshots')}
>
<ha-switch
.checked=${this._config?.menu_buttons?.download ?? true}
.configValue=${'menu_buttons.download'}
.checked=${this._config?.menu?.buttons?.snapshots ??
defaults.menu.buttons.snapshots}
.configValue=${'menu.buttons.snapshots'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
@@ -426,11 +426,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<ha-formfield
.label=${localize('editor.show_button') +
': ' +
localize('menu.frigate_ui')}
localize('config.view.views.image')}
>
<ha-switch
.checked=${this._config?.menu_buttons?.frigate_ui ?? true}
.configValue=${'menu_buttons.frigate_ui'}
.checked=${this._config?.menu?.buttons?.image ??
defaults.menu.buttons.image}
.configValue=${'menu.buttons.image'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
@@ -438,11 +439,38 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
<ha-formfield
.label=${localize('editor.show_button') +
': ' +
localize('menu.fullscreen')}
localize('config.menu.buttons.download')}
>
<ha-switch
.checked=${this._config?.menu_buttons?.frigate_ui ?? true}
.configValue=${'menu_buttons.fullscreen'}
.checked=${this._config?.menu?.buttons?.download ??
defaults.menu.buttons.download}
.configValue=${'menu.buttons.download'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
<br />
<ha-formfield
.label=${localize('editor.show_button') +
': ' +
localize('config.menu.buttons.frigate_ui')}
>
<ha-switch
.checked=${this._config?.menu?.buttons?.frigate_ui ??
defaults.menu.buttons.frigate_ui}
.configValue=${'menu.buttons.frigate_ui'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
<br />
<ha-formfield
.label=${localize('editor.show_button') +
': ' +
localize('config.menu.buttons.fullscreen')}
>
<ha-switch
.checked=${this._config?.menu?.buttons?.fullscreen ??
defaults.menu.buttons.fullscreen}
.configValue=${'menu.buttons.fullscreen'}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
@@ -458,17 +486,17 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${options.advanced.show
? html` <div class="values">
<paper-input
.label=${localize('editor.zone')}
.value=${this._config?.zone || ''}
.configValue=${'zone'}
.label=${localize('config.frigate.label')}
.value=${this._config?.frigate?.label || ''}
.configValue=${'frigate.label'}
@value-changed=${this._valueChanged}
></paper-input>
</div>
<div class="values">
<paper-input
.label=${localize('editor.label')}
.value=${this._config?.label || ''}
.configValue=${'label'}
.label=${localize('config.frigate.zone')}
.value=${this._config?.frigate?.zone || ''}
.configValue=${'frigate.zone'}
@value-changed=${this._valueChanged}
></paper-input>
</div>`
@@ -483,9 +511,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${options.webrtc.show
? html` <div class="values">
<paper-dropdown-menu
.label=${localize('webrtc.entity')}
.label=${localize('config.live.webrtc.entity')}
@value-changed=${this._valueChanged}
.configValue=${'webrtc.entity'}
.configValue=${'live.webrtc.entity'}
>
<paper-listbox
slot="dropdown-content"
@@ -497,9 +525,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</paper-listbox>
</paper-dropdown-menu>
<paper-input
label=${localize('webrtc.url')}
.value=${this._config?.webrtc?.url || ''}
.configValue=${'webrtc.url'}
label=${localize('config.live.webrtc.url')}
.value=${this._config?.live?.webrtc?.url || ''}
.configValue=${'live.webrtc.url'}
@value-changed=${this._valueChanged}
></paper-input>
</div>`
@@ -549,13 +577,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
let objectTarget = newConfig;
if (key.includes('.')) {
const parts = key.split('.', 2);
const configName = parts[0];
if (!(configName in newConfig)) {
newConfig[configName] = {};
const parts = key.split('.');
objectTarget = parts.slice(0, -1).reduce((obj, key) => {
if (!(key in obj)) {
obj[key] = {};
}
objectTarget = newConfig[configName];
key = parts[1];
return obj[key];
}, newConfig);
key = parts[parts.length-1];
}
if (value !== undefined && objectTarget[key] === value) {
+93 -72
View File
@@ -1,15 +1,103 @@
{
"common": {
"version": "Version",
"show_warning": "Show Warning",
"show_error": "Show Error",
"frigate_card": "Frigate Card",
"show_warning": "Show warning",
"show_error": "Show error",
"frigate_card": "Frigate card",
"frigate_card_description": "A Lovelace card for use with Frigate",
"no_snapshots": "No snapshots",
"no_clips": "No clips",
"no_snapshot": "No recent snapshot",
"no_clip": "No recent clip"
},
"config": {
"camera_entity": "Camera Entity",
"frigate": {
"camera_name": "Frigate camera name (Optional, autodetected from entity)",
"client_id": "Frigate client id (For >1 Frigate server)",
"label": "Label/object filter",
"url": "Frigate server URL",
"zone": "Zone"
},
"view": {
"default": "Default view",
"views": {
"live": "Live view",
"clip": "Most recent clip",
"clips": "Clips gallery",
"snapshot": "Most recent snapshot",
"snapshots": "Snapshots gallery",
"image": "Static image"
},
"timeout": "View timeout (seconds)"
},
"event_viewer": {
"autoplay_clip": "Autoplay most recent clip",
"lazy_load": "Lazily load event media",
"controls": {
"next_previous": {
"style": "Event Viewer Next & Previous Control style",
"size": "Event Viewer Next & Previous Control size",
"styles": {
"thumbnails": "Thumbnails",
"chevrons": "Chevrons",
"none": "None"
}
}
}
},
"live": {
"preload": "Preload live view",
"provider": "Live view provider",
"providers": {
"frigate": "Frigate",
"frigate-jsmpeg": "Frigate JSMpeg",
"webrtc": "WebRTC"
},
"webrtc": {
"entity": "WebRTC Camera Entity (Not a Frigate camera)",
"url": "WebRTC Camera URL"
}
},
"image": {
"src": "Static image URL/data for image view"
},
"menu": {
"buttons": {
"frigate": "Frigate menu / Default view",
"frigate_ui": "Frigate user Interface",
"fullscreen": "Fullscreen",
"download": "Download event media"
},
"mode": "Menu mode",
"modes": {
"none": "No menu",
"hidden-top": "Hidden top",
"hidden-left": "Hidden left",
"hidden-bottom": "Hidden bottom",
"hidden-right": "Hidden right",
"overlay-top": "Overlay top",
"overlay-left": "Overlay left",
"overlay-bottom": "Overlay bottom",
"overlay-right": "Overlay right",
"hover-top": "Hover top",
"hover-left": "Hover left",
"hover-bottom": "Hover bottom",
"hover-right": "Hover right",
"above": "Above",
"below": "Below"
}
},
"dimensions": {
"aspect_ratio": "Default aspect ratio",
"aspect_ratio_mode": "Aspect ratio mode",
"aspect_ratio_modes": {
"unconstrained": "Unconstrained aspect ratio",
"dynamic": "Aspect ratio adjusts to media",
"static": "Static aspect ratio"
}
}
},
"editor": {
"basic": "Basic",
"basic_secondary": "Basic options for most users",
@@ -21,74 +109,7 @@
"webrtc_secondary": "Optional WebRTC parameters",
"advanced": "Advanced",
"advanced_secondary": "Advanced options",
"camera_entity": "Camera Entity (Optional)",
"frigate_camera_name": "Frigate camera name (Optional, autodetected from entity)",
"default_view": "Default view (Optional)",
"frigate_client_id": "Frigate client id (Optional, for >1 Frigate server)",
"view_timeout": "View timeout (seconds)",
"frigate_url": "Frigate URL (Optional, for Frigate UI button)",
"autoplay_clip": "Autoplay latest clip",
"menu_mode": "Menu mode (Optional)",
"show_button": "Show button",
"zone": "Zone",
"label": "Frigate label/object filter (Optional)",
"live_provider": "Live view provider (Optional)",
"live_preload": "Preload live view",
"image": "Static image URL for image view (Optional)",
"lazy_load": "Lazily load event media"
},
"menu": {
"frigate": "Frigate Menu / Default View",
"live": "Live View",
"clips": "Clips Gallery",
"snapshots": "Snapshots Gallery",
"clip": "Latest Clip",
"snapshot": "Latest Snapshot",
"frigate_ui": "Frigate User Interface",
"fullscreen": "Fullscreen",
"image": "Static Image",
"download": "Download event media"
},
"control": {
"nextprev": "Media Next & Previous Controls",
"thumbnails": "Thumbnails",
"chevrons": "Chevrons",
"none": "None"
},
"dimensions": {
"aspect_ratio_mode": "Aspect Ratio Mode",
"aspect_ratio": "Default aspect ratio (Optional)"
},
"aspect_ratio_mode": {
"unconstrained": "Unconstrained aspect ratio",
"dynamic": "Aspect ratio adjusts to media",
"static": "Static aspect ratio"
},
"menu_mode": {
"none": "No menu",
"hidden-top": "Hidden Top",
"hidden-left": "Hidden Left",
"hidden-bottom": "Hidden Bottom",
"hidden-right": "Hidden Right",
"overlay-top": "Overlay Top",
"overlay-left": "Overlay Left",
"overlay-bottom": "Overlay Bottom",
"overlay-right": "Overlay Right",
"hover-top": "Hover Top",
"hover-left": "Hover Left",
"hover-bottom": "Hover Bottom",
"hover-right": "Hover Right",
"above": "Above",
"below": "Below"
},
"live_provider": {
"frigate": "Frigate",
"frigate-jsmpeg": "Frigate JSMpeg",
"webrtc": "WebRTC"
},
"webrtc": {
"entity": "Optional WebRTC Camera Entity (Not a Frigate camera)",
"url": "Optional WebRTC Camera URL"
"show_button": "Show button"
},
"error": {
"empty_response": "Received empty response from Home Assistant for request",
@@ -101,7 +122,7 @@
"invalid_configuration": "Invalid configuration",
"invalid_configuration_no_hint": "No location hint available (bad or missing type?)",
"missing_webrtc": "WebRTC component not found",
"no_frigate_camera_name": "Cannot autodetect Frigate camera name, you need to either set camera_entity and / or frigate_camera_name",
"no_frigate_camera_name": "Cannot autodetect Frigate camera name, you need to either set camera_entity and / or frigate.camera_name",
"could_not_render_elements": "Could not render picture elements",
"invalid_elements_config": "Invalid picture elements configuration",
"jsmpeg_no_sign": "Could not retrieve or sign JSMPEG websocket path",
+5
View File
@@ -1,6 +1,11 @@
:host {
--video-max-height: none;
}
ha-hls-player {
transform-style: preserve-3d;
}
img,video {
width: 100%;
height: 100%;
+172 -52
View File
@@ -38,7 +38,7 @@ export type FrigateCardView =
| typeof FRIGATE_CARD_VIEWS_USER_SPECIFIED[number]
| typeof FRIGATE_CARD_VIEWS_INTERNAL[number];
export const FRIGATE_MENU_MODES = [
const FRIGATE_MENU_MODES = [
'none',
'hidden-top',
'hidden-left',
@@ -55,13 +55,8 @@ export const FRIGATE_MENU_MODES = [
'above',
'below',
] as const;
export type FrigateMenuMode = typeof FRIGATE_MENU_MODES[number];
export const NEXT_PREVIOUS_CONTROL_STYLES = ['none', 'thumbnails', 'chevrons'] as const;
export type NextPreviousControlStyle = typeof NEXT_PREVIOUS_CONTROL_STYLES[number];
export const LIVE_PROVIDERS = ['frigate', 'frigate-jsmpeg', 'webrtc'] as const;
export type LiveProvider = typeof LIVE_PROVIDERS[number];
/**
* Action Types (for "Picture Elements" / Menu)
@@ -122,7 +117,7 @@ const elementsBaseSchema = z.object({
});
/**
* Picture Element Types
* Picture Element Configuration.
*
* All picture element types are validated (not just the Frigate card custom
* ones) as a convenience to present the user with a consistent error display
@@ -214,7 +209,7 @@ const customSchema = z
if (!val.match(/^custom:(?!frigate-card).+/)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Frigate-card custom elements must match specific schemas",
message: 'Frigate-card custom elements must match specific schemas',
fatal: true,
});
}
@@ -223,7 +218,7 @@ const customSchema = z
.passthrough();
/**
* Custom Element Types
* Custom Element Types.
*/
export const menuIconSchema = iconSchema.merge(
@@ -269,15 +264,37 @@ export type PictureElement = z.infer<typeof pictureElementSchema>;
const pictureElementsSchema = pictureElementSchema.array().optional();
export type PictureElements = z.infer<typeof pictureElementsSchema>;
export const frigateCardConfigSchema = z.object({
camera_entity: z.string().optional(),
/**
* Frigate configuration section.
*/
const frigateConfigDefault = {
client_id: 'frigate' as const,
};
const frigateConfigDefaultSchema = z
.object({
// No URL validation to allow relative URLs within HA (e.g. addons).
frigate_url: z.string().optional(),
frigate_client_id: z.string().optional().default('frigate'),
frigate_camera_name: z.string().optional(),
view_default: z.enum(FRIGATE_CARD_VIEWS_USER_SPECIFIED).optional().default('live'),
view_timeout: z
url: z.string().optional(),
client_id: z.string().optional().default(frigateConfigDefault.client_id),
camera_name: z.string().optional(),
label: z.string().optional(),
zone: z.string().optional(),
})
.default(frigateConfigDefault);
/**
* View configuration section.
*/
const viewConfigDefault = {
default: 'live' as const,
timeout: 180,
};
const viewConfigSchema = z
.object({
default: z
.enum(FRIGATE_CARD_VIEWS_USER_SPECIFIED)
.optional()
.default(viewConfigDefault.default),
timeout: z
.number()
.or(
z
@@ -286,52 +303,129 @@ export const frigateCardConfigSchema = z.object({
.transform((val) => Number(val)),
)
.optional()
.default(180),
live_provider: z.enum(LIVE_PROVIDERS).default('frigate'),
live_preload: z.boolean().default(false),
image: z.string().optional(),
webrtc: z
.default(viewConfigDefault.timeout),
})
.default(viewConfigDefault);
/**
* Image view configuration section.
*/
const imageConfigSchema = z
.object({
src: z.string().optional(),
})
.optional();
export type ImageViewConfig = z.infer<typeof imageConfigSchema>;
/**
* Live view configuration section.
*/
const liveConfigDefault = {
provider: 'frigate' as const,
preload: false,
};
const webrtcConfigSchema = z
.object({
entity: z.string().optional(),
url: z.string().optional(),
})
.passthrough()
.optional(),
label: z.string().optional(),
zone: z.string().optional(),
autoplay_clip: z.boolean().default(false),
event_viewer: z
.optional();
export type WebRTCConfig = z.infer<typeof webrtcConfigSchema>;
const liveConfigSchema = z
.object({
lazy_load: z.boolean().default(true),
provider: z.enum(LIVE_PROVIDERS).default(liveConfigDefault.provider),
preload: z.boolean().default(liveConfigDefault.preload),
webrtc: webrtcConfigSchema,
})
.optional(),
menu_mode: z.enum(FRIGATE_MENU_MODES).optional().default('hidden-top'),
menu_buttons: z
.default(liveConfigDefault);
/**
* Menu configuration section.
*/
const menuConfigDefault = {
mode: 'hidden-top' as const,
buttons: {
frigate: true,
live: true,
clips: true,
snapshots: true,
image: false,
download: true,
frigate_ui: true,
fullscreen: true,
},
button_size: '40px',
};
const menuConfigSchema = z
.object({
frigate: z.boolean().default(true),
live: z.boolean().default(true),
clips: z.boolean().default(true),
snapshots: z.boolean().default(true),
image: z.boolean().default(false),
download: z.boolean().default(true),
frigate_ui: z.boolean().default(true),
fullscreen: z.boolean().default(true),
mode: z.enum(FRIGATE_MENU_MODES).optional().default(menuConfigDefault.mode),
buttons: z
.object({
frigate: z.boolean().default(menuConfigDefault.buttons.frigate),
live: z.boolean().default(menuConfigDefault.buttons.live),
clips: z.boolean().default(menuConfigDefault.buttons.clips),
snapshots: z.boolean().default(menuConfigDefault.buttons.snapshots),
image: z.boolean().default(menuConfigDefault.buttons.image),
download: z.boolean().default(menuConfigDefault.buttons.download),
frigate_ui: z.boolean().default(menuConfigDefault.buttons.frigate_ui),
fullscreen: z.boolean().default(menuConfigDefault.buttons.fullscreen),
})
.optional(),
menu_button_size: z.string().default('40px'),
update_entities: z.string().array().optional(),
elements: pictureElementsSchema,
.default(menuConfigDefault.buttons),
button_size: z.string().default(menuConfigDefault.button_size),
})
.default(menuConfigDefault);
export type MenuConfig = z.infer<typeof menuConfigSchema>;
/**
* Event viewer configuration section (clip, snapshot, clip-specific, snapshot-specific).
*/
const viewerConfigDefault = {
autoplay_clip: false,
lazy_load: true,
controls: {
next_previous: {
size: '48px',
style: 'thumbnails' as const,
},
},
};
const nextPreviousControlConfigSchema = z
.object({
style: z
.enum(NEXT_PREVIOUS_CONTROL_STYLES)
.default(viewerConfigDefault.controls.next_previous.style),
size: z.string().default(viewerConfigDefault.controls.next_previous.size),
})
.default(viewerConfigDefault.controls.next_previous);
export type NextPreviousControlConfig = z.infer<typeof nextPreviousControlConfigSchema>;
const viewerConfigSchema = z
.object({
autoplay_clip: z.boolean().default(viewerConfigDefault.autoplay_clip),
lazy_load: z.boolean().default(viewerConfigDefault.lazy_load),
controls: z
.object({
nextprev: z.enum(NEXT_PREVIOUS_CONTROL_STYLES).default('thumbnails'),
nextprev_size: z.string().default('48px'),
next_previous: nextPreviousControlConfigSchema,
})
.optional(),
dimensions: z
.default(viewerConfigDefault.controls),
})
.default(viewerConfigDefault);
export type ViewerConfig = z.infer<typeof viewerConfigSchema>;
/**
* Dimensions configuration section.
*/
const dimensionsConfigDefault = {
aspect_ratio_mode: 'dynamic' as const,
aspect_ratio: [16, 9],
};
const dimensionsConfigSchema = z
.object({
aspect_ratio_mode: z
.enum(['dynamic', 'static', 'unconstrained'])
.default('dynamic'),
.default(dimensionsConfigDefault.aspect_ratio_mode),
aspect_ratio: z
.number()
.array()
@@ -342,9 +436,27 @@ export const frigateCardConfigSchema = z.object({
.regex(/^\s*\d+\s*[:\/]\s*\d+\s*$/)
.transform((input) => input.split(/[:\/]/).map((d) => Number(d))),
)
.default([16, 9]),
.default(dimensionsConfigDefault.aspect_ratio),
})
.optional(),
.default(dimensionsConfigDefault);
/**
* Main card config.
*/
export const frigateCardConfigSchema = z.object({
camera_entity: z.string().optional(),
frigate: frigateConfigDefaultSchema,
view: viewConfigSchema,
menu: menuConfigSchema,
live: liveConfigSchema,
event_viewer: viewerConfigSchema,
image: imageConfigSchema,
elements: pictureElementsSchema,
dimensions: dimensionsConfigSchema,
// Entities that should trigger a card update.
update_entities: z.string().array().optional(),
// Stock lovelace card config.
type: z.string(),
@@ -354,6 +466,14 @@ export const frigateCardConfigSchema = z.object({
});
export type FrigateCardConfig = z.infer<typeof frigateCardConfigSchema>;
export const frigateCardConfigDefaults = {
frigate: frigateConfigDefault,
view: viewConfigDefault,
menu: menuConfigDefault,
live: liveConfigDefault,
event_viewer: viewerConfigDefault,
};
// Schema for card (non-user configured) menu icons.
const internalMenuIconSchema = z.object({
type: z.literal('internal-menu-icon'),
@@ -404,8 +524,8 @@ export interface Message {
}
export interface MenuInteraction {
interaction: "hold" | "tap" | "double_tap",
button: MenuButton,
interaction: 'hold' | 'tap' | 'double_tap';
button: MenuButton;
}
/**