Significant rework in how events are updated from components.
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cycjimmy/jsmpeg-player": "^5.0.1",
|
"@cycjimmy/jsmpeg-player": "^5.0.1",
|
||||||
|
"@lit-labs/task": "^1.0.0",
|
||||||
"@material/image-list": "^12.0.0",
|
"@material/image-list": "^12.0.0",
|
||||||
"@material/mwc-menu": "^0.25.3",
|
"@material/mwc-menu": "^0.25.3",
|
||||||
"@material/rtl": "^13.0.0",
|
"@material/rtl": "^13.0.0",
|
||||||
|
|||||||
@@ -77,12 +77,9 @@ export class BrowseMediaUtil {
|
|||||||
* @returns A BrowseMediaSource object or null on malformed.
|
* @returns A BrowseMediaSource object or null on malformed.
|
||||||
*/
|
*/
|
||||||
static async browseMedia(
|
static async browseMedia(
|
||||||
hass: (HomeAssistant & ExtendedHomeAssistant) | null,
|
hass: HomeAssistant & ExtendedHomeAssistant,
|
||||||
media_content_id: string,
|
media_content_id: string,
|
||||||
): Promise<BrowseMediaSource | null> {
|
): Promise<BrowseMediaSource> {
|
||||||
if (!hass) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const request = {
|
const request = {
|
||||||
type: 'media_source/browse_media',
|
type: 'media_source/browse_media',
|
||||||
media_content_id: media_content_id,
|
media_content_id: media_content_id,
|
||||||
@@ -101,7 +98,7 @@ export class BrowseMediaUtil {
|
|||||||
static async browseMediaQuery(
|
static async browseMediaQuery(
|
||||||
hass: HomeAssistant & ExtendedHomeAssistant,
|
hass: HomeAssistant & ExtendedHomeAssistant,
|
||||||
params: BrowseMediaQueryParameters,
|
params: BrowseMediaQueryParameters,
|
||||||
): Promise<BrowseMediaSource | null> {
|
): Promise<BrowseMediaSource> {
|
||||||
return this.browseMedia(
|
return this.browseMedia(
|
||||||
hass,
|
hass,
|
||||||
// Defined in:
|
// Defined in:
|
||||||
|
|||||||
+1
-1
@@ -600,7 +600,7 @@ export class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _changeView(args?: { view?: View; resetMessage?: boolean }): void {
|
protected _changeView(args?: { view?: View; resetMessage?: boolean }): void {
|
||||||
console.info(`Request to change view: ${JSON.stringify(args?.view)}`)
|
console.info(`Request to change view: ${JSON.stringify(args?.view?.view)}`)
|
||||||
|
|
||||||
if (args?.resetMessage ?? true) {
|
if (args?.resetMessage ?? true) {
|
||||||
this._message = null;
|
this._message = null;
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ export async function homeAssistantWSRequest<T>(
|
|||||||
hass: HomeAssistant & ExtendedHomeAssistant,
|
hass: HomeAssistant & ExtendedHomeAssistant,
|
||||||
schema: ZodSchema<T>,
|
schema: ZodSchema<T>,
|
||||||
request: MessageBase,
|
request: MessageBase,
|
||||||
): Promise<T | null> {
|
): Promise<T> {
|
||||||
const response = await hass.callWS<T>(request);
|
const response = await hass.callWS<T>(request);
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
|
|||||||
@@ -35,24 +35,13 @@ export class FrigateCardCarousel extends LitElement {
|
|||||||
updated(changedProperties: PropertyValues): void {
|
updated(changedProperties: PropertyValues): void {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
if (this._shouldInitCarousel(changedProperties)) {
|
if (!this._carousel) {
|
||||||
this.updateComplete.then(() => {
|
this.updateComplete.then(() => {
|
||||||
this._initCarousel();
|
this._initCarousel();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the carousel should be (re-)initialized when the given
|
|
||||||
* properties change.
|
|
||||||
* @param changedProperties The properties that triggered the (re-)render.
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
protected _shouldInitCarousel(_: PropertyValues): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Embla options to use.
|
* Get the Embla options to use.
|
||||||
* @returns An EmblaOptionsType object or undefined for no options.
|
* @returns An EmblaOptionsType object or undefined for no options.
|
||||||
@@ -61,6 +50,13 @@ export class FrigateCardCarousel extends LitElement {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _destroyCarousel(): void {
|
||||||
|
if (this._carousel) {
|
||||||
|
this._carousel.destroy();
|
||||||
|
}
|
||||||
|
this._carousel = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the carousel with "slides".
|
* Load the carousel with "slides".
|
||||||
*/
|
*/
|
||||||
@@ -70,9 +66,6 @@ export class FrigateCardCarousel extends LitElement {
|
|||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
|
|
||||||
if (carouselNode) {
|
if (carouselNode) {
|
||||||
if (this._carousel) {
|
|
||||||
this._carousel.destroy();
|
|
||||||
}
|
|
||||||
this._carousel = EmblaCarousel(carouselNode, this._getOptions());
|
this._carousel = EmblaCarousel(carouselNode, this._getOptions());
|
||||||
this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init'));
|
this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init'));
|
||||||
this._carousel.on('select', () => {
|
this._carousel.on('select', () => {
|
||||||
|
|||||||
+59
-29
@@ -1,6 +1,7 @@
|
|||||||
// TODO Live scrolling nav buttons disappearing
|
// TODO Live scrolling nav buttons disappearing
|
||||||
// TODO conditional elements based on camera name (requires event changed to propagate upwards)
|
// TODO Media loading events appear wrong for snapshot viewer
|
||||||
// TODO _shouldInitCarousel on viewer carousel and thumbnail carousel.
|
// TODO can I do away with clip/snapshot-specific?
|
||||||
|
// TODO Live carousel chevron/icons style
|
||||||
// TODO call change-event in viewer
|
// TODO call change-event in viewer
|
||||||
// TODO editor for live lazy loading
|
// TODO editor for live lazy loading
|
||||||
// TODO different live configs per camera
|
// TODO different live configs per camera
|
||||||
@@ -72,7 +73,7 @@ export class FrigateCardLive extends LitElement {
|
|||||||
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected view?: View;
|
protected view?: Readonly<View>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected cameras?: Map<string, CameraConfig>;
|
protected cameras?: Map<string, CameraConfig>;
|
||||||
@@ -207,7 +208,7 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
|||||||
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected view?: View;
|
protected view?: Readonly<View>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected cameras?: Map<string, CameraConfig>;
|
protected cameras?: Map<string, CameraConfig>;
|
||||||
@@ -215,19 +216,34 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected liveConfig?: LiveConfig;
|
protected liveConfig?: LiveConfig;
|
||||||
|
|
||||||
|
// Index between camera name and slide number.
|
||||||
|
protected _cameraToSlide: Record<string, number> = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the carousel should be (re-)initialized when the given
|
* The updated lifecycle callback for this element.
|
||||||
* properties change.
|
* @param changedProperties The properties that were changed in this render.
|
||||||
* @param changedProperties The properties that triggered the (re-)render.
|
|
||||||
* @returns Whether to re-initialize the carousel.
|
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
updated(changedProperties: PropertyValues): void {
|
||||||
protected _shouldInitCarousel(changedProps: PropertyValues): boolean {
|
if (changedProperties.has('cameras') || changedProperties.has('liveConfig')) {
|
||||||
// These are the only properties that would cause new cameras or changed
|
this._destroyCarousel();
|
||||||
// dimensions. Don't allow other properties to re-initialize the carousel as
|
}
|
||||||
// it's a jarring experience to the user (and 'view' is itself set as a
|
|
||||||
// result of a carousel move).
|
if (changedProperties.has('view')) {
|
||||||
return changedProps.has('cameras') || changedProps.has('liveConfig');
|
const oldView = changedProperties.get('view') as View | undefined;
|
||||||
|
if (
|
||||||
|
this._carousel &&
|
||||||
|
oldView &&
|
||||||
|
this.view?.camera &&
|
||||||
|
this.view?.camera != oldView.camera
|
||||||
|
) {
|
||||||
|
const slide: number | undefined = this._cameraToSlide[this.view.camera];
|
||||||
|
if (slide !== undefined && slide !== this.carouselSelected()) {
|
||||||
|
this.carouselScrollTo(slide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.updated(changedProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -260,15 +276,25 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get slides to include in the render.
|
* Get slides to include in the render.
|
||||||
* @returns The slides to include in the render.
|
* @returns The slides to include in the render and an index keyed by camera
|
||||||
|
* name to slide number.
|
||||||
*/
|
*/
|
||||||
protected _getSlides(): TemplateResult[] {
|
protected _getSlides(): [TemplateResult[], Record<string, number>] {
|
||||||
if (!this.cameras) {
|
if (!this.cameras) {
|
||||||
return [];
|
return [[], {}];
|
||||||
}
|
}
|
||||||
return Array.from(this.cameras.values()).map((cameraConfig, index) => {
|
|
||||||
return this._renderLive(cameraConfig, index);
|
const slides: TemplateResult[] = [];
|
||||||
});
|
const cameraToSlide: Record<string, number> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of this.cameras) {
|
||||||
|
const slide = this._renderLive(value, slides.length);
|
||||||
|
if (slide) {
|
||||||
|
cameraToSlide[key] = slides.length;
|
||||||
|
slides.push(slide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [slides, cameraToSlide];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -280,17 +306,20 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedSnap = this._carousel.selectedScrollSnap();
|
const selectedSnap = this._carousel.selectedScrollSnap();
|
||||||
const newView = this.view.clone();
|
this.view
|
||||||
newView.camera = Array.from(this.cameras.keys())[selectedSnap];
|
.evolve({
|
||||||
newView.previous = this.view;
|
camera: Array.from(this.cameras.keys())[selectedSnap],
|
||||||
newView.dispatchChangeEvent(this);
|
previous: this.view,
|
||||||
|
})
|
||||||
|
.dispatchChangeEvent(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazy load a slide.
|
* Lazy load a slide.
|
||||||
* @param _slide The slide to lazy load.
|
* @param _index The slide number to lazy load.
|
||||||
|
* @param slide The slide to lazy load.
|
||||||
*/
|
*/
|
||||||
protected _lazyLoadSlide(slide: HTMLElement): void {
|
protected _lazyLoadSlide(_index: number, slide: HTMLElement): void {
|
||||||
const liveProvider = slide.querySelector(
|
const liveProvider = slide.querySelector(
|
||||||
'frigate-card-live-provider',
|
'frigate-card-live-provider',
|
||||||
) as FrigateCardLiveProvider;
|
) as FrigateCardLiveProvider;
|
||||||
@@ -306,7 +335,7 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${cameraConfig}
|
.cameraConfig=${cameraConfig}
|
||||||
.liveConfig=${this.liveConfig}
|
.liveConfig=${this.liveConfig}
|
||||||
?disabled=${this._getLazyLoadCount() != null}
|
?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)}
|
||||||
>
|
>
|
||||||
@@ -365,7 +394,8 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
|||||||
* @returns A template to display to the user.
|
* @returns A template to display to the user.
|
||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
const slides = this._getSlides();
|
const [slides, cameraToSlide] = this._getSlides();
|
||||||
|
this._cameraToSlide = cameraToSlide;
|
||||||
if (!slides) {
|
if (!slides) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,29 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if lazy loading is being used.
|
||||||
|
* @returns `true` is lazy loading is in use.
|
||||||
|
*/
|
||||||
|
protected _isLazyLoading(): boolean {
|
||||||
|
return this._getLazyLoadCount() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _destroyCarousel(): void {
|
||||||
|
super._destroyCarousel();
|
||||||
|
|
||||||
|
// Notes on instance variables:
|
||||||
|
// * this._mediaShowInfo: This is set when the media in the DOM loads. If a
|
||||||
|
// new View included the same media, the DOM would not change and so the
|
||||||
|
// prior contents would still be valid and would not re-appear (as the
|
||||||
|
// media would not reload) -- as such, leave this alone on carousel
|
||||||
|
// destroy. New media in that slide will replace the prior contents on
|
||||||
|
// load.
|
||||||
|
// * this._slideHasBeenLazyLoaded: This is a performance optimization and
|
||||||
|
// can be safely reset.
|
||||||
|
this._slideHasBeenLazyLoaded = {};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializse the carousel with "slides" (clips or snapshots).
|
* Initializse the carousel with "slides" (clips or snapshots).
|
||||||
*/
|
*/
|
||||||
@@ -158,16 +181,17 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._slideHasBeenLazyLoaded[index] = true;
|
this._slideHasBeenLazyLoaded[index] = true;
|
||||||
this._lazyLoadSlide(slides[index]);
|
this._lazyLoadSlide(index, slides[index]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazy load a slide.
|
* Lazy load a slide.
|
||||||
|
* @param _index The index of the slide to lazy load.
|
||||||
* @param _slide The slide to lazy load.
|
* @param _slide The slide to lazy load.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
protected _lazyLoadSlide(_slide: HTMLElement): void {
|
protected _lazyLoadSlide(_index: number, _slide: HTMLElement): void {
|
||||||
// To be overridden in children.
|
// To be overridden in children.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +237,7 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
|||||||
mediaShowInfo?: MediaShowInfo | null,
|
mediaShowInfo?: MediaShowInfo | null,
|
||||||
): void {
|
): void {
|
||||||
// isValidMediaShowInfo is used to prevent saving media info that will be
|
// isValidMediaShowInfo is used to prevent saving media info that will be
|
||||||
// rejected upstream.
|
// rejected upstream (empty 1x1 images will be rejected here).
|
||||||
if (mediaShowInfo && isValidMediaShowInfo(mediaShowInfo)) {
|
if (mediaShowInfo && isValidMediaShowInfo(mediaShowInfo)) {
|
||||||
this._mediaShowInfo[slideIndex] = mediaShowInfo;
|
this._mediaShowInfo[slideIndex] = mediaShowInfo;
|
||||||
if (this._carousel && this._carousel?.slidesInView(true).includes(slideIndex)) {
|
if (this._carousel && this._carousel?.slidesInView(true).includes(slideIndex)) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { customElement, property } from 'lit/decorators.js';
|
|||||||
|
|
||||||
import type { BrowseMediaSource, ThumbnailsControlConfig } from '../types.js';
|
import type { BrowseMediaSource, ThumbnailsControlConfig } from '../types.js';
|
||||||
import { FrigateCardCarousel } from './carousel.js';
|
import { FrigateCardCarousel } from './carousel.js';
|
||||||
import { actionHandler } from '../action-handler-directive.js';
|
|
||||||
import { dispatchFrigateCardEvent } from '../common.js';
|
import { dispatchFrigateCardEvent } from '../common.js';
|
||||||
|
|
||||||
import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss';
|
import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss';
|
||||||
@@ -112,11 +111,7 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
|||||||
|
|
||||||
return html`<div
|
return html`<div
|
||||||
class="embla__slide"
|
class="embla__slide"
|
||||||
.actionHandler=${actionHandler({
|
@click=${() => {
|
||||||
hasHold: false,
|
|
||||||
hasDoubleClick: false,
|
|
||||||
})}
|
|
||||||
@action=${() => {
|
|
||||||
if (this._carousel && this._carousel.clickAllowed()) {
|
if (this._carousel && this._carousel.clickAllowed()) {
|
||||||
dispatchFrigateCardEvent<ThumbnailCarouselTap>(this, 'carousel:tap', {
|
dispatchFrigateCardEvent<ThumbnailCarouselTap>(this, 'carousel:tap', {
|
||||||
slideIndex: slideIndex,
|
slideIndex: slideIndex,
|
||||||
|
|||||||
+244
-168
@@ -1,11 +1,18 @@
|
|||||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
import {
|
||||||
|
CSSResultGroup,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
html,
|
||||||
|
unsafeCSS,
|
||||||
|
} from 'lit';
|
||||||
import { BrowseMediaUtil } from '../browse-media-util.js';
|
import { BrowseMediaUtil } from '../browse-media-util.js';
|
||||||
import { EmblaOptionsType } from 'embla-carousel';
|
import { EmblaOptionsType } from 'embla-carousel';
|
||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { Task } from '@lit-labs/task';
|
||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { until } from 'lit/directives/until.js';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
BrowseMediaNeighbors,
|
BrowseMediaNeighbors,
|
||||||
@@ -17,16 +24,16 @@ import type {
|
|||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { FrigateCardMediaCarousel, IMG_EMPTY } from './media-carousel.js';
|
import { FrigateCardMediaCarousel, IMG_EMPTY } from './media-carousel.js';
|
||||||
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
|
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
|
||||||
import { FrigateCardThumbnailCarousel, ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
import {
|
||||||
|
FrigateCardThumbnailCarousel,
|
||||||
|
ThumbnailCarouselTap,
|
||||||
|
} from './thumbnail-carousel.js';
|
||||||
import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js';
|
import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js';
|
||||||
import { View } from '../view.js';
|
import { View } from '../view.js';
|
||||||
import { actionHandler } from '../action-handler-directive.js';
|
|
||||||
import {
|
import {
|
||||||
createMediaShowInfo,
|
createMediaShowInfo,
|
||||||
dispatchErrorMessageEvent,
|
dispatchErrorMessageEvent,
|
||||||
dispatchMessageEvent,
|
dispatchMessageEvent,
|
||||||
dispatchPauseEvent,
|
|
||||||
dispatchPlayEvent,
|
|
||||||
} from '../common.js';
|
} from '../common.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import { renderProgressIndicator } from '../components/message.js';
|
import { renderProgressIndicator } from '../components/message.js';
|
||||||
@@ -42,7 +49,7 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected view?: View;
|
protected view?: Readonly<View>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected viewerConfig?: ViewerConfig;
|
protected viewerConfig?: ViewerConfig;
|
||||||
@@ -54,28 +61,41 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
protected resolvedMediaCache?: ResolvedMediaCache;
|
protected resolvedMediaCache?: ResolvedMediaCache;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve all the given media for a target.
|
* Asyncronously render the element.
|
||||||
* @param target The target to resolve media from.
|
* @returns A rendered template.
|
||||||
* @returns True if the resolutions were all error free.
|
|
||||||
*/
|
*/
|
||||||
protected async _resolveAllMediaForTarget(
|
protected async _fetchLatestMedia(): Promise<void> {
|
||||||
target: BrowseMediaSource,
|
if (!this.view || !this.hass || !this.browseMediaQueryParameters) {
|
||||||
): Promise<boolean> {
|
return;
|
||||||
if (!this.hass) {
|
}
|
||||||
return false;
|
let parent: BrowseMediaSource | null;
|
||||||
|
try {
|
||||||
|
parent = await BrowseMediaUtil.browseMediaQuery(
|
||||||
|
this.hass,
|
||||||
|
this.browseMediaQueryParameters,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return dispatchErrorMessageEvent(this, (e as Error).message);
|
||||||
|
}
|
||||||
|
const childIndex = BrowseMediaUtil.getFirstTrueMediaChildIndex(parent);
|
||||||
|
if (!parent || !parent.children || childIndex == null) {
|
||||||
|
return dispatchMessageEvent(
|
||||||
|
this,
|
||||||
|
this.browseMediaQueryParameters.mediaType == 'clips'
|
||||||
|
? localize('common.no_clip')
|
||||||
|
: localize('common.no_snapshot'),
|
||||||
|
this.browseMediaQueryParameters.mediaType == 'clips'
|
||||||
|
? 'mdi:filmstrip-off'
|
||||||
|
: 'mdi:camera-off',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let errorFree = true;
|
this.view
|
||||||
for (let i = 0; target.children && i < (target.children || []).length; ++i) {
|
.evolve({
|
||||||
if (BrowseMediaUtil.isTrueMedia(target.children[i])) {
|
target: parent,
|
||||||
errorFree &&= !!(await ResolvedMediaUtil.resolveMedia(
|
childIndex: childIndex,
|
||||||
this.hass,
|
})
|
||||||
target.children[i],
|
.dispatchChangeEvent(this);
|
||||||
this.resolvedMediaCache,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errorFree;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,44 +103,13 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
return html`${until(this._render(), renderProgressIndicator())}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asyncronously render the element.
|
|
||||||
* @returns A rendered template.
|
|
||||||
*/
|
|
||||||
protected async _render(): Promise<TemplateResult | void> {
|
|
||||||
if (!this.hass || !this.view || !this.browseMediaQueryParameters) {
|
if (!this.hass || !this.view || !this.browseMediaQueryParameters) {
|
||||||
return html``;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.view.is('clip') || this.view.is('snapshot')) {
|
if (!this.view.target) {
|
||||||
let parent: BrowseMediaSource | null = null;
|
this._fetchLatestMedia();
|
||||||
try {
|
return renderProgressIndicator();
|
||||||
parent = await BrowseMediaUtil.browseMediaQuery(
|
|
||||||
this.hass,
|
|
||||||
this.browseMediaQueryParameters,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
return dispatchErrorMessageEvent(this, (e as Error).message);
|
|
||||||
}
|
|
||||||
const childIndex = BrowseMediaUtil.getFirstTrueMediaChildIndex(parent);
|
|
||||||
if (!parent || !parent.children || childIndex == null) {
|
|
||||||
return dispatchMessageEvent(
|
|
||||||
this,
|
|
||||||
this.view.is('clip')
|
|
||||||
? localize('common.no_clip')
|
|
||||||
: localize('common.no_snapshot'),
|
|
||||||
this.view.is('clip') ? 'mdi:filmstrip-off' : 'mdi:camera-off',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.view.target = parent;
|
|
||||||
this.view.childIndex = childIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.view.target && !(await this._resolveAllMediaForTarget(this.view.target))) {
|
|
||||||
return dispatchErrorMessageEvent(this, localize('error.could_not_resolve'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return html` <frigate-card-viewer-core
|
return html` <frigate-card-viewer-core
|
||||||
@@ -147,7 +136,7 @@ export class FrigateCardViewerCore extends LitElement {
|
|||||||
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected view?: View;
|
protected view?: Readonly<View>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected viewerConfig?: ViewerConfig;
|
protected viewerConfig?: ViewerConfig;
|
||||||
@@ -222,7 +211,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected view?: View;
|
protected view?: Readonly<View>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected viewerConfig?: ViewerConfig;
|
protected viewerConfig?: ViewerConfig;
|
||||||
@@ -237,20 +226,86 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
// (Folders are not media items that can be rendered).
|
// (Folders are not media items that can be rendered).
|
||||||
protected _slideToChild: Record<number, number> = {};
|
protected _slideToChild: Record<number, number> = {};
|
||||||
|
|
||||||
|
// A task to resolve target media if lazy loading is disabled.
|
||||||
|
protected _mediaResolutionTask = new Task<[BrowseMediaSource | undefined], void>(
|
||||||
|
this,
|
||||||
|
async ([target]: (BrowseMediaSource | undefined)[]): Promise<void> => {
|
||||||
|
for (
|
||||||
|
let i = 0;
|
||||||
|
!this._isLazyLoading() &&
|
||||||
|
this.hass &&
|
||||||
|
target &&
|
||||||
|
target.children &&
|
||||||
|
i < (target.children || []).length;
|
||||||
|
++i
|
||||||
|
) {
|
||||||
|
if (BrowseMediaUtil.isTrueMedia(target.children[i])) {
|
||||||
|
await ResolvedMediaUtil.resolveMedia(
|
||||||
|
this.hass,
|
||||||
|
target.children[i],
|
||||||
|
this.resolvedMediaCache,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => [this.view?.target],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The updated lifecycle callback for this element.
|
||||||
|
* @param changedProperties The properties that were changed in this render.
|
||||||
|
*/
|
||||||
|
updated(changedProperties: PropertyValues): void {
|
||||||
|
if (changedProperties.has('viewerConfig')) {
|
||||||
|
this._destroyCarousel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProperties.has('view')) {
|
||||||
|
const oldView = changedProperties.get('view') as View | undefined;
|
||||||
|
if (oldView) {
|
||||||
|
if (oldView.target != this.view?.target) {
|
||||||
|
// If the media target is different entirely, reset the carousel.
|
||||||
|
this._destroyCarousel();
|
||||||
|
} else if (this._carousel && this.view?.childIndex != oldView.childIndex) {
|
||||||
|
const slide = this._getSlideForChild(this.view?.childIndex);
|
||||||
|
if (slide !== undefined && slide !== this.carouselSelected()) {
|
||||||
|
// If the media target is the same as already loaded, but isn't of
|
||||||
|
// the selected slide, scroll to that slide.
|
||||||
|
this.carouselScrollTo(slide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.updated(changedProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _destroyCarousel(): void {
|
||||||
|
super._destroyCarousel();
|
||||||
|
|
||||||
|
// Notes on instance variables:
|
||||||
|
// * this._slideToChild: This is set as part of each render and does not
|
||||||
|
// need to be destroyed here.
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _getSlideForChild(childIndex: number | undefined): number | undefined {
|
||||||
|
if (childIndex === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const slideIndex = Object.keys(this._slideToChild).find(
|
||||||
|
(key) => this._slideToChild[key] === childIndex,
|
||||||
|
);
|
||||||
|
return slideIndex !== undefined ? Number(slideIndex) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Embla options to use.
|
* Get the Embla options to use.
|
||||||
* @returns An EmblaOptionsType object or undefined for no options.
|
* @returns An EmblaOptionsType object or undefined for no options.
|
||||||
*/
|
*/
|
||||||
protected _getOptions(): EmblaOptionsType {
|
protected _getOptions(): EmblaOptionsType {
|
||||||
// Start the carousel on the selected child number.
|
|
||||||
const startIndex = Number(
|
|
||||||
Object.keys(this._slideToChild).find(
|
|
||||||
(key) => this._slideToChild[key] === this.view?.childIndex,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
startIndex: isNaN(startIndex) ? undefined : startIndex,
|
// Start the carousel on the selected child number.
|
||||||
|
startIndex: this._getSlideForChild(this.view?.childIndex),
|
||||||
draggable: this.viewerConfig?.draggable,
|
draggable: this.viewerConfig?.draggable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -262,7 +317,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
* should load simultaneously.
|
* should load simultaneously.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
protected _getLazyLoadCount(): number | null {
|
protected _getLazyLoadCount(): number | null {
|
||||||
// Defaults to fully-lazy loading.
|
// Defaults to fully-lazy loading.
|
||||||
return this.viewerConfig?.lazy_load === false ? null : 0;
|
return this.viewerConfig?.lazy_load === false ? null : 0;
|
||||||
}
|
}
|
||||||
@@ -414,54 +469,77 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
if (slidesInView.length) {
|
if (slidesInView.length) {
|
||||||
const childIndex = this._slideToChild[slidesInView[0]];
|
const childIndex = this._slideToChild[slidesInView[0]];
|
||||||
if (childIndex !== undefined) {
|
if (childIndex !== undefined) {
|
||||||
// Update the currently live view in place.
|
this.view
|
||||||
this.view.childIndex = childIndex;
|
.evolve({
|
||||||
|
childIndex: childIndex,
|
||||||
|
previous: this.view,
|
||||||
|
})
|
||||||
|
.dispatchChangeEvent(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazy load a slide.
|
* Lazy load a slide.
|
||||||
|
* @param index The index of the slide to lazy load.
|
||||||
* @param slide The slide to lazy load.
|
* @param slide The slide to lazy load.
|
||||||
*/
|
*/
|
||||||
protected _lazyLoadSlide(slide: HTMLElement): void {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected _lazyLoadSlide(index: number, slide: HTMLElement): void {
|
||||||
|
const childIndex: number | undefined = this._slideToChild[index];
|
||||||
|
|
||||||
// Snapshots.
|
if (
|
||||||
const img = slide.querySelector('img') as HTMLImageElement;
|
childIndex == undefined ||
|
||||||
|
!this.hass ||
|
||||||
// Frigate >= 0.9.0+ clips.
|
!this.view ||
|
||||||
const hls_player = slide.querySelector(
|
!this.view.target ||
|
||||||
'frigate-card-ha-hls-player',
|
!this.view.target.children ||
|
||||||
) as HTMLElement & { url: string };
|
!BrowseMediaUtil.isTrueMedia(this.view.target.children[childIndex])
|
||||||
|
) {
|
||||||
// Frigate < 0.9.0 clips. frigate-card-ha-hls-player will also have a
|
return;
|
||||||
// video source element, so search for that first.
|
|
||||||
const video_source = slide.querySelector('video source') as HTMLElement & {
|
|
||||||
src: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (img) {
|
|
||||||
img.src = img.getAttribute('data-src') || img.src;
|
|
||||||
} else if (hls_player) {
|
|
||||||
hls_player.url = hls_player.getAttribute('data-url') || hls_player.url;
|
|
||||||
} else if (video_source) {
|
|
||||||
video_source.src = video_source.getAttribute('data-src') || video_source.src;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResolvedMediaUtil.resolveMedia(
|
||||||
|
this.hass,
|
||||||
|
this.view.target.children[childIndex],
|
||||||
|
this.resolvedMediaCache,
|
||||||
|
).then((resolvedMedia) => {
|
||||||
|
if (!resolvedMedia) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshots.
|
||||||
|
const img = slide.querySelector('img') as HTMLImageElement;
|
||||||
|
|
||||||
|
// Frigate >= 0.9.0+ clips.
|
||||||
|
const hls_player = slide.querySelector(
|
||||||
|
'frigate-card-ha-hls-player',
|
||||||
|
) as HTMLElement & { url: string };
|
||||||
|
|
||||||
|
if (img) {
|
||||||
|
img.src = resolvedMedia.url;
|
||||||
|
} else if (hls_player) {
|
||||||
|
hls_player.url = resolvedMedia.url;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle updating of the next/previous controls when the carousel is moved.
|
* Handle updating of the next/previous controls when the carousel is moved.
|
||||||
*/
|
*/
|
||||||
protected _selectSlideNextPreviousHandler(): void {
|
protected _selectSlideNextPreviousHandler(): void {
|
||||||
const updateNextPreviousControl = (control: FrigateCardNextPreviousControl, direction: 'previous' | 'next'): void => {
|
const updateNextPreviousControl = (
|
||||||
|
control: FrigateCardNextPreviousControl,
|
||||||
|
direction: 'previous' | 'next',
|
||||||
|
): void => {
|
||||||
const neighbors = this._getMediaNeighbors();
|
const neighbors = this._getMediaNeighbors();
|
||||||
const [prev, next] = [neighbors?.previous, neighbors?.next]
|
const [prev, next] = [neighbors?.previous, neighbors?.next];
|
||||||
const target = direction == 'previous' ? prev : next;
|
const target = direction == 'previous' ? prev : next;
|
||||||
|
|
||||||
control.disabled = (target == null)
|
control.disabled = target == null;
|
||||||
control.title = (target && target.title ? target.title : '')
|
control.title = target && target.title ? target.title : '';
|
||||||
control.thumbnail = (target && target.thumbnail ? target.thumbnail : undefined)
|
control.thumbnail = target && target.thumbnail ? target.thumbnail : undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
if (this._previousControlRef.value) {
|
if (this._previousControlRef.value) {
|
||||||
updateNextPreviousControl(this._previousControlRef.value, 'previous');
|
updateNextPreviousControl(this._previousControlRef.value, 'previous');
|
||||||
@@ -473,43 +551,76 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get slides to include in the render.
|
* Get slides to include in the render.
|
||||||
* @returns The slides to include in the render.
|
* @returns The slides to include in the render and an index keyed by slide
|
||||||
|
* number that maps to child number.
|
||||||
*/
|
*/
|
||||||
protected _getSlides(): TemplateResult[] {
|
protected _getSlides(): [TemplateResult[], Record<number, number>] {
|
||||||
if (
|
if (
|
||||||
!this.view ||
|
!this.view ||
|
||||||
!this.view.target ||
|
!this.view.target ||
|
||||||
!this.view.target.children ||
|
!this.view.target.children ||
|
||||||
!this.view.target.children.length
|
!this.view.target.children.length
|
||||||
) {
|
) {
|
||||||
return [];
|
return [[], {}];
|
||||||
}
|
}
|
||||||
|
|
||||||
this._slideToChild = {};
|
const slideToChild: Record<number, number> = {};
|
||||||
const slides: TemplateResult[] = [];
|
const slides: TemplateResult[] = [];
|
||||||
for (let i = 0; i < this.view.target.children?.length; ++i) {
|
for (let i = 0; i < this.view.target.children?.length; ++i) {
|
||||||
const slide = this._renderMediaItem(this.view.target.children[i], slides.length);
|
const slide = this._renderMediaItem(this.view.target.children[i], slides.length);
|
||||||
|
|
||||||
if (slide) {
|
if (slide) {
|
||||||
this._slideToChild[slides.length] = i;
|
slideToChild[slides.length] = i;
|
||||||
slides.push(slide);
|
slides.push(slide);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return slides;
|
return [slides, slideToChild];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if all the media in the carousel are resolved.
|
||||||
|
*/
|
||||||
|
protected _isMediaFullyResolved(): boolean {
|
||||||
|
for (const child of this.view?.target?.children || []) {
|
||||||
|
if (!this.resolvedMediaCache?.has(child.media_content_id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the element, resolving the media first if necessary.
|
||||||
|
*/
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
this._slideToChild = {};
|
||||||
|
|
||||||
|
// If lazy loading is not enabled, wait for the media resolver task to
|
||||||
|
// complete and show a progress indictator until this.
|
||||||
|
if (!this._isLazyLoading() && !this._isMediaFullyResolved()) {
|
||||||
|
return html`${this._mediaResolutionTask.render({
|
||||||
|
initial: () => renderProgressIndicator(),
|
||||||
|
pending: () => renderProgressIndicator(),
|
||||||
|
error: (e: unknown) => dispatchErrorMessageEvent(this, (e as Error).message),
|
||||||
|
complete: () => this._render(),
|
||||||
|
})}`;
|
||||||
|
}
|
||||||
|
return this._render();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the element.
|
* Render the element.
|
||||||
* @returns A template to display to the user.
|
* @returns A template to display to the user.
|
||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected _render(): TemplateResult | void {
|
||||||
const slides = this._getSlides();
|
const [slides, slideToChild] = this._getSlides();
|
||||||
|
this._slideToChild = slideToChild;
|
||||||
if (!slides) {
|
if (!slides) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const neighbors = this._getMediaNeighbors();
|
const neighbors = this._getMediaNeighbors();
|
||||||
const [prev, next] = [neighbors?.previous, neighbors?.next]
|
const [prev, next] = [neighbors?.previous, neighbors?.next];
|
||||||
|
|
||||||
return html`<div class="embla">
|
return html`<div class="embla">
|
||||||
<frigate-card-next-previous-control
|
<frigate-card-next-previous-control
|
||||||
@@ -540,13 +651,11 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected _renderMediaItem(
|
protected _renderMediaItem(
|
||||||
mediaToRender: BrowseMediaSource,
|
mediaToRender: BrowseMediaSource,
|
||||||
slideIndex: number,
|
slideIndex: number,
|
||||||
): TemplateResult | void {
|
): TemplateResult | void {
|
||||||
// media that can be expanded (folders) cannot be resolved to a single media
|
// Skip folders as they cannot be rendered by this viewer.
|
||||||
// item, skip them.
|
|
||||||
if (
|
if (
|
||||||
!this.view ||
|
!this.view ||
|
||||||
!this.viewerConfig ||
|
!this.viewerConfig ||
|
||||||
@@ -555,8 +664,9 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lazyLoad = this._isLazyLoading();
|
||||||
const resolvedMedia = this.resolvedMediaCache?.get(mediaToRender.media_content_id);
|
const resolvedMedia = this.resolvedMediaCache?.get(mediaToRender.media_content_id);
|
||||||
if (!resolvedMedia) {
|
if (!resolvedMedia && !lazyLoad) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,53 +680,26 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
autoplay = this.viewerConfig.autoplay_clip;
|
autoplay = this.viewerConfig.autoplay_clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lazyLoad = (this._getLazyLoadCount() !== null);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="embla__slide">
|
<div class="embla__slide">
|
||||||
${this.view.isClipRelatedView()
|
${this.view.isClipRelatedView()
|
||||||
? resolvedMedia?.mime_type.toLowerCase() == 'application/x-mpegurl'
|
? html`<frigate-card-ha-hls-player
|
||||||
? html`<frigate-card-ha-hls-player
|
.hass=${this.hass}
|
||||||
.hass=${this.hass}
|
url=${ifDefined(lazyLoad ? undefined : resolvedMedia?.url)}
|
||||||
url=${ifDefined(lazyLoad ? undefined : resolvedMedia.url)}
|
|
||||||
data-url=${ifDefined(lazyLoad ? resolvedMedia.url : undefined)}
|
|
||||||
title="${mediaToRender.title}"
|
|
||||||
muted
|
|
||||||
controls
|
|
||||||
playsinline
|
|
||||||
allow-exoplayer
|
|
||||||
?autoplay="${autoplay}"
|
|
||||||
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) =>
|
|
||||||
this._mediaShowEventHandler(slideIndex, e)}
|
|
||||||
>
|
|
||||||
</frigate-card-ha-hls-player>`
|
|
||||||
: html`<video
|
|
||||||
title="${mediaToRender.title}"
|
|
||||||
muted
|
|
||||||
controls
|
|
||||||
playsinline
|
|
||||||
?autoplay="${autoplay}"
|
|
||||||
@loadedmetadata="${(e: Event) => {
|
|
||||||
this._mediaLoadedHandler(slideIndex, createMediaShowInfo(e));
|
|
||||||
}}"
|
|
||||||
@play=${() => dispatchPlayEvent(this)}
|
|
||||||
@pause=${() => dispatchPauseEvent(this)}
|
|
||||||
>
|
|
||||||
<source
|
|
||||||
src=${ifDefined(lazyLoad ? undefined : resolvedMedia.url)}
|
|
||||||
data-src=${ifDefined(lazyLoad ? resolvedMedia.url : undefined)}
|
|
||||||
type="${resolvedMedia.mime_type}"
|
|
||||||
/>
|
|
||||||
</video>`
|
|
||||||
: html`<img
|
|
||||||
src=${ifDefined(lazyLoad ? IMG_EMPTY : resolvedMedia.url)}
|
|
||||||
data-src=${ifDefined(lazyLoad ? resolvedMedia.url : undefined)}
|
|
||||||
title="${mediaToRender.title}"
|
title="${mediaToRender.title}"
|
||||||
.actionHandler=${actionHandler({
|
muted
|
||||||
hasHold: false,
|
controls
|
||||||
hasDoubleClick: false,
|
playsinline
|
||||||
})}
|
allow-exoplayer
|
||||||
@action=${() => {
|
?autoplay="${autoplay}"
|
||||||
|
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) =>
|
||||||
|
this._mediaShowEventHandler(slideIndex, e)}
|
||||||
|
>
|
||||||
|
</frigate-card-ha-hls-player>`
|
||||||
|
: html`<img
|
||||||
|
src=${ifDefined(lazyLoad ? IMG_EMPTY : resolvedMedia?.url)}
|
||||||
|
title="${mediaToRender.title}"
|
||||||
|
@click=${() => {
|
||||||
if (this._carousel?.clickAllowed()) {
|
if (this._carousel?.clickAllowed()) {
|
||||||
this._findRelatedClipView(mediaToRender).then((view) => {
|
this._findRelatedClipView(mediaToRender).then((view) => {
|
||||||
if (view) {
|
if (view) {
|
||||||
@@ -626,14 +709,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@load="${(e: Event) => {
|
@load="${(e: Event) => {
|
||||||
if (
|
this._mediaLoadedHandler(slideIndex, createMediaShowInfo(e));
|
||||||
this.viewerConfig &&
|
|
||||||
// This handler will be called on the empty image, only call
|
|
||||||
// the below when it's the 'real image'.
|
|
||||||
(!lazyLoad || this._slideHasBeenLazyLoaded[slideIndex])
|
|
||||||
) {
|
|
||||||
this._mediaLoadedHandler(slideIndex, createMediaShowInfo(e));
|
|
||||||
}
|
|
||||||
}}"
|
}}"
|
||||||
/>`}
|
/>`}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
"frigate_ui": "Frigate user Interface",
|
"frigate_ui": "Frigate user Interface",
|
||||||
"fullscreen": "Fullscreen",
|
"fullscreen": "Fullscreen",
|
||||||
"download": "Download event media",
|
"download": "Download event media",
|
||||||
"cameras": "Camera selection"
|
"cameras": "Select camera"
|
||||||
},
|
},
|
||||||
"mode": "Menu mode",
|
"mode": "Menu mode",
|
||||||
"modes": {
|
"modes": {
|
||||||
|
|||||||
+29
-3
@@ -1,14 +1,19 @@
|
|||||||
import type { BrowseMediaSource, FrigateCardView } from './types.js';
|
import type { BrowseMediaSource, FrigateCardView } from './types.js';
|
||||||
import { dispatchFrigateCardEvent } from './common.js';
|
import { dispatchFrigateCardEvent } from './common.js';
|
||||||
|
|
||||||
export interface ViewParameters {
|
export interface ViewEvolveParameters {
|
||||||
view: FrigateCardView;
|
view?: FrigateCardView;
|
||||||
camera: string;
|
camera?: string;
|
||||||
target?: BrowseMediaSource;
|
target?: BrowseMediaSource;
|
||||||
childIndex?: number;
|
childIndex?: number;
|
||||||
previous?: View;
|
previous?: View;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ViewParameters extends ViewEvolveParameters {
|
||||||
|
view: FrigateCardView;
|
||||||
|
camera: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class View {
|
export class View {
|
||||||
view: FrigateCardView;
|
view: FrigateCardView;
|
||||||
camera: string;
|
camera: string;
|
||||||
@@ -24,6 +29,9 @@ export class View {
|
|||||||
this.previous = params?.previous;
|
this.previous = params?.previous;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone a view.
|
||||||
|
*/
|
||||||
public clone(): View {
|
public clone(): View {
|
||||||
return new View({
|
return new View({
|
||||||
view: this.view,
|
view: this.view,
|
||||||
@@ -34,6 +42,24 @@ export class View {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evolve this view by changing parameters and returning a new view.
|
||||||
|
* @param params Parameters to change.
|
||||||
|
* @returns A new evolved view.
|
||||||
|
*/
|
||||||
|
public evolve(params: ViewEvolveParameters): View {
|
||||||
|
return new View({
|
||||||
|
view: params.view ?? this.view,
|
||||||
|
camera: params.camera ?? this.camera,
|
||||||
|
target: params.target ?? this.target,
|
||||||
|
childIndex: params.childIndex ?? this.childIndex,
|
||||||
|
previous: params.previous ?? this.previous,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if current view matches a named view.
|
||||||
|
*/
|
||||||
public is(name: string): boolean {
|
public is(name: string): boolean {
|
||||||
return this.view == name;
|
return this.view == name;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user