Initial code for major engine refactor.
This commit is contained in:
+20
-36
@@ -41,15 +41,12 @@ export class FrigateCardCarousel extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public carouselPlugins?: EmblaCarouselPlugins;
|
||||
|
||||
@property({ attribute: false })
|
||||
public selected = 0;
|
||||
|
||||
@property({ attribute: true })
|
||||
public transitionEffect?: TransitionEffect;
|
||||
|
||||
// An override to the startIndex, used to preserve the current carousel
|
||||
// position after the carousel is destroyed (so it can be restored if
|
||||
// recreated).
|
||||
// See: https://github.com/dermotduffy/frigate-hass-card/issues/775
|
||||
protected _savedStartIndex: number | null = null;
|
||||
|
||||
protected _refSlot: Ref<HTMLSlotElement> = createRef();
|
||||
|
||||
protected _carousel?: EmblaCarouselType;
|
||||
@@ -81,7 +78,7 @@ export class FrigateCardCarousel extends LitElement {
|
||||
// Destroy the carousel when the component is disconnected, which forces the
|
||||
// plugins (which may have registered event handlers) to also be destroyed.
|
||||
// The carousel will automatically reconstruct if the component is re-rendered.
|
||||
this._destroyCarousel({ savePosition: true });
|
||||
this._destroyCarousel();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
@@ -96,7 +93,7 @@ export class FrigateCardCarousel extends LitElement {
|
||||
'carouselPlugins',
|
||||
] as const;
|
||||
if (destroyProperties.some((prop) => changedProps.has(prop))) {
|
||||
this._destroyCarousel({ savePosition: true });
|
||||
this._destroyCarousel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,31 +102,21 @@ export class FrigateCardCarousel extends LitElement {
|
||||
* @param index Slide number.
|
||||
*/
|
||||
public carouselScrollTo(index: number): void {
|
||||
const scroll = () =>
|
||||
this._carousel?.scrollTo(index, this.transitionEffect === 'none');
|
||||
// This ensures scrolling can work on initial render when the carousel may
|
||||
// not yet exist.
|
||||
if (this._carousel) {
|
||||
scroll();
|
||||
} else {
|
||||
this.updateComplete.then(() => {
|
||||
scroll();
|
||||
});
|
||||
}
|
||||
this.selected = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the previous slide.
|
||||
*/
|
||||
public carouselScrollPrevious(): void {
|
||||
this._carousel?.scrollPrev(this.transitionEffect === 'none');
|
||||
this.selected = Math.max(0, this.selected - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the next slide.
|
||||
*/
|
||||
public carouselScrollNext(): void {
|
||||
this._carousel?.scrollNext(this.transitionEffect === 'none');
|
||||
this.selected = this.selected + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,11 +161,10 @@ export class FrigateCardCarousel extends LitElement {
|
||||
window.requestAnimationFrame(() => {
|
||||
this._carousel?.reInit({ ...options });
|
||||
});
|
||||
}
|
||||
const selected = this.getCarouselSelected();
|
||||
};
|
||||
|
||||
carouselReInit({
|
||||
...(selected && { startIndex: selected.index }),
|
||||
startIndex: this.selected,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,6 +197,10 @@ export class FrigateCardCarousel extends LitElement {
|
||||
if (!this._carousel) {
|
||||
this._initCarousel();
|
||||
}
|
||||
|
||||
if (changedProperties.has('selected')) {
|
||||
this._carousel?.scrollTo(this.selected, this.transitionEffect === 'none');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,9 +208,7 @@ export class FrigateCardCarousel extends LitElement {
|
||||
* @param options If `savePosition` is set the existing carousel position
|
||||
* will be saved so it can be restored if the carousel is recreated.
|
||||
*/
|
||||
protected _destroyCarousel(options?: { savePosition: boolean }): void {
|
||||
this._savedStartIndex =
|
||||
(options?.savePosition ? this._carousel?.selectedScrollSnap() : null) ?? null;
|
||||
protected _destroyCarousel(): void {
|
||||
if (this._carousel) {
|
||||
this._carousel.destroy();
|
||||
}
|
||||
@@ -248,8 +236,8 @@ export class FrigateCardCarousel extends LitElement {
|
||||
{
|
||||
axis: this.direction == 'horizontal' ? 'x' : 'y',
|
||||
speed: 20,
|
||||
startIndex: this.selected,
|
||||
...this.carouselOptions,
|
||||
...(this._savedStartIndex !== null && { startIndex: this._savedStartIndex }),
|
||||
},
|
||||
this.carouselPlugins,
|
||||
);
|
||||
@@ -262,7 +250,7 @@ export class FrigateCardCarousel extends LitElement {
|
||||
// Make sure every select causes a refresh to allow for re-paint of the
|
||||
// next/previous controls.
|
||||
this.requestUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
this._carousel.on('init', selectSlide);
|
||||
this._carousel.on('select', selectSlide);
|
||||
@@ -294,18 +282,14 @@ export class FrigateCardCarousel extends LitElement {
|
||||
protected _slotChanged(): void {
|
||||
// Cannot just re-init, because the slide elements themselves may have
|
||||
// changed, and only a carousel init can pass in new (slotted) children. If
|
||||
// the slides themselves change, any position the user has set is assumed to
|
||||
// be abandoned and so the startIndex is reset to whatever the carousel was
|
||||
// originally configured with.
|
||||
this._destroyCarousel({ savePosition: false });
|
||||
this._destroyCarousel();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const slides = this._refSlot.value?.assignedElements({ flatten: true }) || [];
|
||||
const currentSlide = (this._carousel?.selectedScrollSnap() ?? this.carouselOptions?.startIndex) ?? 0;
|
||||
const showPrevious = this.carouselOptions?.loop || currentSlide > 0;
|
||||
const showNext = this.carouselOptions?.loop || currentSlide + 1 < slides.length;
|
||||
const showPrevious = this.carouselOptions?.loop || this.selected > 0;
|
||||
const showNext = this.carouselOptions?.loop || this.selected + 1 < slides.length;
|
||||
|
||||
return html` <div class="embla">
|
||||
${showPrevious ? html`<slot name="previous"></slot>` : ``}
|
||||
|
||||
@@ -126,7 +126,7 @@ export class FrigateCardDrawer extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
<slot ${ref(this._refSlot)} @slotchange=${this._slotChanged.bind(this)}></slot>
|
||||
<slot ${ref(this._refSlot)} @slotchange=${() => this._slotChanged()}></slot>
|
||||
</side-drawer>
|
||||
`;
|
||||
}
|
||||
|
||||
+230
-238
@@ -19,12 +19,10 @@ import {
|
||||
} from '../types.js';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
import {
|
||||
fetchChildMediaAndDispatchViewChange,
|
||||
fetchLatestMediaAndDispatchViewChange,
|
||||
getFullDependentBrowseMediaQueryParametersOrDispatchError,
|
||||
} from '../utils/ha/browse-media';
|
||||
import { changeViewToRecentRecordingForCameraAndDependents } from '../utils/media-to-view.js';
|
||||
import { DataManager } from '../utils/data-manager.js';
|
||||
import { changeViewToRecentEventsForCameraAndDependents, changeViewToRecentRecordingForCameraAndDependents } from '../utils/media-to-view.js';
|
||||
import { DataManager } from '../utils/data/data-manager.js';
|
||||
import { View } from '../view.js';
|
||||
import { renderProgressIndicator } from './message.js';
|
||||
import './thumbnail.js';
|
||||
@@ -66,63 +64,54 @@ export class FrigateCardGallery extends LitElement {
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
const mediaType = this.view?.getMediaType();
|
||||
if (
|
||||
!this.hass ||
|
||||
!this.view ||
|
||||
!this.cameras ||
|
||||
!this.view.isGalleryView() ||
|
||||
!mediaType ||
|
||||
!this.dataManager
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// const mediaType = this.view?.getMediaType();
|
||||
// if (
|
||||
// !this.hass ||
|
||||
// !this.view ||
|
||||
// !this.cameras ||
|
||||
// !this.view.isGalleryView() ||
|
||||
// !mediaType ||
|
||||
// !this.dataManager
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!this.view.target) {
|
||||
if (mediaType === 'recordings') {
|
||||
changeViewToRecentRecordingForCameraAndDependents(
|
||||
this,
|
||||
this.hass,
|
||||
this.dataManager,
|
||||
this.cameras,
|
||||
this.view,
|
||||
{
|
||||
targetView: 'recordings',
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const browseMediaQueryParameters =
|
||||
getFullDependentBrowseMediaQueryParametersOrDispatchError(
|
||||
this,
|
||||
this.hass,
|
||||
this.cameras,
|
||||
this.view.camera,
|
||||
mediaType,
|
||||
);
|
||||
// if (!this.view.query) {
|
||||
// if (mediaType === 'recordings') {
|
||||
// changeViewToRecentRecordingForCameraAndDependents(
|
||||
// this,
|
||||
// this.hass,
|
||||
// this.dataManager,
|
||||
// this.cameras,
|
||||
// this.view,
|
||||
// {
|
||||
// targetView: 'recordings',
|
||||
// },
|
||||
// );
|
||||
// } else {
|
||||
// changeViewToRecentEventsForCameraAndDependents(
|
||||
// this,
|
||||
// this.hass,
|
||||
// this.dataManager,
|
||||
// this.cameras,
|
||||
// this.view,
|
||||
// {
|
||||
// targetView: mediaType,
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// return renderProgressIndicator({ cardWideConfig: this.cardWideConfig });
|
||||
// }
|
||||
|
||||
if (!browseMediaQueryParameters) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchLatestMediaAndDispatchViewChange(
|
||||
this,
|
||||
this.hass,
|
||||
this.view,
|
||||
browseMediaQueryParameters,
|
||||
);
|
||||
}
|
||||
return renderProgressIndicator({ cardWideConfig: this.cardWideConfig });
|
||||
}
|
||||
|
||||
return html`
|
||||
<frigate-card-gallery-core
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.galleryConfig=${this.galleryConfig}
|
||||
.cameras=${this.cameras}
|
||||
>
|
||||
</frigate-card-gallery-core>
|
||||
`;
|
||||
// return html`
|
||||
// <frigate-card-gallery-core
|
||||
// .hass=${this.hass}
|
||||
// .view=${this.view}
|
||||
// .galleryConfig=${this.galleryConfig}
|
||||
// .cameras=${this.cameras}
|
||||
// >
|
||||
// </frigate-card-gallery-core>
|
||||
// `;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,203 +128,206 @@ export class FrigateCardGallery extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('frigate-card-gallery-core')
|
||||
export class FrigateCardGalleryCore extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: ExtendedHomeAssistant;
|
||||
// @customElement('frigate-card-gallery-core')
|
||||
// export class FrigateCardGalleryCore extends LitElement {
|
||||
// @property({ attribute: false })
|
||||
// public hass?: ExtendedHomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public view?: Readonly<View>;
|
||||
// @property({ attribute: false })
|
||||
// public view?: Readonly<View>;
|
||||
|
||||
@property({ attribute: false })
|
||||
public galleryConfig?: GalleryConfig;
|
||||
// @property({ attribute: false })
|
||||
// public galleryConfig?: GalleryConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameras?: Map<string, CameraConfig>;
|
||||
// @property({ attribute: false })
|
||||
// public cameras?: Map<string, CameraConfig>;
|
||||
|
||||
protected _resizeObserver: ResizeObserver;
|
||||
// protected _resizeObserver: ResizeObserver;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._resizeObserver = new ResizeObserver(this._resizeHandler.bind(this));
|
||||
}
|
||||
// constructor() {
|
||||
// super();
|
||||
// this._resizeObserver = new ResizeObserver(this._resizeHandler.bind(this));
|
||||
// }
|
||||
|
||||
/**
|
||||
* Component connected callback.
|
||||
*/
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._resizeObserver.observe(this);
|
||||
}
|
||||
// /**
|
||||
// * Component connected callback.
|
||||
// */
|
||||
// connectedCallback(): void {
|
||||
// super.connectedCallback();
|
||||
// this._resizeObserver.observe(this);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Component disconnected callback.
|
||||
*/
|
||||
disconnectedCallback(): void {
|
||||
this._resizeObserver.disconnect();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
// /**
|
||||
// * Component disconnected callback.
|
||||
// */
|
||||
// disconnectedCallback(): void {
|
||||
// this._resizeObserver.disconnect();
|
||||
// super.disconnectedCallback();
|
||||
// }
|
||||
|
||||
/**
|
||||
* Set gallery columns.
|
||||
*/
|
||||
protected _setColumnCount(): void {
|
||||
const thumbnailSize =
|
||||
this.galleryConfig?.controls.thumbnails.size ??
|
||||
frigateCardConfigDefaults.event_gallery.controls.thumbnails.size;
|
||||
const columns = this.galleryConfig?.controls.thumbnails.show_details
|
||||
? Math.max(1, Math.floor(this.clientWidth / THUMBNAIL_DETAILS_WIDTH_MIN))
|
||||
: Math.max(
|
||||
1,
|
||||
Math.ceil(this.clientWidth / THUMBNAIL_WIDTH_MAX),
|
||||
Math.ceil(this.clientWidth / thumbnailSize),
|
||||
);
|
||||
// /**
|
||||
// * Set gallery columns.
|
||||
// */
|
||||
// protected _setColumnCount(): void {
|
||||
// const thumbnailSize =
|
||||
// this.galleryConfig?.controls.thumbnails.size ??
|
||||
// frigateCardConfigDefaults.event_gallery.controls.thumbnails.size;
|
||||
// const columns = this.galleryConfig?.controls.thumbnails.show_details
|
||||
// ? Math.max(1, Math.floor(this.clientWidth / THUMBNAIL_DETAILS_WIDTH_MIN))
|
||||
// : Math.max(
|
||||
// 1,
|
||||
// Math.ceil(this.clientWidth / THUMBNAIL_WIDTH_MAX),
|
||||
// Math.ceil(this.clientWidth / thumbnailSize),
|
||||
// );
|
||||
|
||||
this.style.setProperty('--frigate-card-gallery-columns', String(columns));
|
||||
}
|
||||
// this.style.setProperty('--frigate-card-gallery-columns', String(columns));
|
||||
// }
|
||||
|
||||
/**
|
||||
* Handle gallery resize.
|
||||
*/
|
||||
protected _resizeHandler(): void {
|
||||
this._setColumnCount();
|
||||
}
|
||||
// /**
|
||||
// * Handle gallery resize.
|
||||
// */
|
||||
// protected _resizeHandler(): void {
|
||||
// this._setColumnCount();
|
||||
// }
|
||||
|
||||
/**
|
||||
* Determine whether the back arrow should be displayed.
|
||||
* @returns `true` if the back arrow should be displayed, `false` otherwise.
|
||||
*/
|
||||
protected _showBackArrow(): boolean {
|
||||
return (
|
||||
!!this.view?.context?.gallery?.previous &&
|
||||
!!this.view.context.gallery.previous.target &&
|
||||
this.view.context.gallery.previous.view === this.view.view
|
||||
);
|
||||
}
|
||||
// /**
|
||||
// * Determine whether the back arrow should be displayed.
|
||||
// * @returns `true` if the back arrow should be displayed, `false` otherwise.
|
||||
// */
|
||||
// protected _shouldShowBackArrow(): boolean {
|
||||
// return (
|
||||
// !!this.view?.context?.gallery?.previous &&
|
||||
// !!this.view.context.gallery.previous.query &&
|
||||
// this.view.context.gallery.previous.view === this.view.view
|
||||
// );
|
||||
// }
|
||||
|
||||
/**
|
||||
* Called when an update will occur.
|
||||
* @param changedProps The changed properties
|
||||
*/
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('galleryConfig')) {
|
||||
if (this.galleryConfig?.controls.thumbnails.show_details) {
|
||||
this.setAttribute('details', '');
|
||||
} else {
|
||||
this.removeAttribute('details');
|
||||
}
|
||||
this._setColumnCount();
|
||||
if (this.galleryConfig?.controls.thumbnails.size) {
|
||||
this.style.setProperty(
|
||||
'--frigate-card-thumbnail-size',
|
||||
`${this.galleryConfig.controls.thumbnails.size}px`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * Called when an update will occur.
|
||||
// * @param changedProps The changed properties
|
||||
// */
|
||||
// protected willUpdate(changedProps: PropertyValues): void {
|
||||
// if (changedProps.has('galleryConfig')) {
|
||||
// if (this.galleryConfig?.controls.thumbnails.show_details) {
|
||||
// this.setAttribute('details', '');
|
||||
// } else {
|
||||
// this.removeAttribute('details');
|
||||
// }
|
||||
// this._setColumnCount();
|
||||
// if (this.galleryConfig?.controls.thumbnails.size) {
|
||||
// this.style.setProperty(
|
||||
// '--frigate-card-thumbnail-size',
|
||||
// `${this.galleryConfig.controls.thumbnails.size}px`,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (
|
||||
!this.hass ||
|
||||
!this.view ||
|
||||
!this.view.target ||
|
||||
!this.view.target.children ||
|
||||
!this.view.isGalleryView() ||
|
||||
!this.cameras
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
// // TODO: This is still going to show the gallery view (akin to HA media
|
||||
// // browser).
|
||||
|
||||
return html`
|
||||
${this._showBackArrow()
|
||||
? html` <ha-card
|
||||
@click=${(ev) => {
|
||||
if (this.view && this.view.context?.gallery?.previous) {
|
||||
this.view.context.gallery.previous.dispatchChangeEvent(this);
|
||||
}
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
outlined=""
|
||||
>
|
||||
<ha-icon .icon=${'mdi:arrow-left'}></ha-icon>
|
||||
</ha-card>`
|
||||
: ''}
|
||||
${this.view.target.children.map(
|
||||
(child, index) =>
|
||||
html`
|
||||
${child.can_expand
|
||||
? html`
|
||||
<ha-card
|
||||
@click=${(ev) => {
|
||||
if (this.hass && this.view) {
|
||||
fetchChildMediaAndDispatchViewChange(
|
||||
this,
|
||||
this.hass,
|
||||
this.view,
|
||||
child,
|
||||
{
|
||||
gallery: {
|
||||
previous: this.view,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
outlined=""
|
||||
>
|
||||
<div>${child.title}</div>
|
||||
</ha-card>
|
||||
`
|
||||
: html`<frigate-card-thumbnail
|
||||
.view=${this.view}
|
||||
.target=${this.view?.target ?? null}
|
||||
.childIndex=${index}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${child.frigate?.cameraID
|
||||
? this.cameras?.get(child.frigate.cameraID)
|
||||
: undefined}
|
||||
?details=${!!this.galleryConfig?.controls.thumbnails.show_details}
|
||||
?show_favorite_control=${!!this.galleryConfig?.controls.thumbnails
|
||||
.show_favorite_control}
|
||||
?show_timeline_control=${!!this.galleryConfig?.controls.thumbnails
|
||||
.show_timeline_control}
|
||||
@click=${(ev: Event) => {
|
||||
if (this.view) {
|
||||
const targetView = this.view.getViewerViewForGalleryView();
|
||||
if (targetView) {
|
||||
this.view
|
||||
.evolve({
|
||||
view: targetView,
|
||||
childIndex: index,
|
||||
})
|
||||
.dispatchChangeEvent(this);
|
||||
}
|
||||
}
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
>
|
||||
</frigate-card-thumbnail>`}
|
||||
`,
|
||||
)}
|
||||
`;
|
||||
}
|
||||
// /**
|
||||
// * Master render method.
|
||||
// * @returns A rendered template.
|
||||
// */
|
||||
// protected render(): TemplateResult | void {
|
||||
// const results = this.view?.queryResults?.getResults();
|
||||
|
||||
/**
|
||||
* Get styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(galleryStyle);
|
||||
}
|
||||
}
|
||||
// if (
|
||||
// !results ||
|
||||
// !this.hass ||
|
||||
// !this.view ||
|
||||
// !this.view.isGalleryView() ||
|
||||
// !this.cameras
|
||||
// ) {
|
||||
// return html``;
|
||||
// }
|
||||
|
||||
// return html`
|
||||
// ${this._shouldShowBackArrow()
|
||||
// ? html` <ha-card
|
||||
// @click=${(ev) => {
|
||||
// if (this.view && this.view.context?.gallery?.previous) {
|
||||
// this.view.context.gallery.previous.dispatchChangeEvent(this);
|
||||
// }
|
||||
// stopEventFromActivatingCardWideActions(ev);
|
||||
// }}
|
||||
// outlined=""
|
||||
// >
|
||||
// <ha-icon .icon=${'mdi:arrow-left'}></ha-icon>
|
||||
// </ha-card>`
|
||||
// : ''}
|
||||
// ${results.map((child, index) =>
|
||||
// html`
|
||||
// ${child.can_expand
|
||||
// ? html`
|
||||
// <ha-card
|
||||
// @click=${(ev) => {
|
||||
// if (this.hass && this.view) {
|
||||
// fetchChildMediaAndDispatchViewChange(
|
||||
// this,
|
||||
// this.hass,
|
||||
// this.view,
|
||||
// child,
|
||||
// {
|
||||
// gallery: {
|
||||
// previous: this.view,
|
||||
// },
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// stopEventFromActivatingCardWideActions(ev);
|
||||
// }}
|
||||
// outlined=""
|
||||
// >
|
||||
// <div>${child.title}</div>
|
||||
// </ha-card>
|
||||
// `
|
||||
// : html`<frigate-card-thumbnail
|
||||
// .view=${this.view}
|
||||
// .target=${this.view?.target ?? null}
|
||||
// .childIndex=${index}
|
||||
// .hass=${this.hass}
|
||||
// .cameraConfig=${child.frigate?.cameraID
|
||||
// ? this.cameras?.get(child.frigate.cameraID)
|
||||
// : undefined}
|
||||
// ?details=${!!this.galleryConfig?.controls.thumbnails.show_details}
|
||||
// ?show_favorite_control=${!!this.galleryConfig?.controls.thumbnails
|
||||
// .show_favorite_control}
|
||||
// ?show_timeline_control=${!!this.galleryConfig?.controls.thumbnails
|
||||
// .show_timeline_control}
|
||||
// @click=${(ev: Event) => {
|
||||
// if (this.view) {
|
||||
// const targetView = this.view.getViewerViewForGalleryView();
|
||||
// if (targetView) {
|
||||
// this.view
|
||||
// .evolve({
|
||||
// view: targetView,
|
||||
// childIndex: index,
|
||||
// })
|
||||
// .dispatchChangeEvent(this);
|
||||
// }
|
||||
// }
|
||||
// stopEventFromActivatingCardWideActions(ev);
|
||||
// }}
|
||||
// >
|
||||
// </frigate-card-thumbnail>`}
|
||||
// `,
|
||||
// )}
|
||||
// `;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Get styles.
|
||||
// */
|
||||
// static get styles(): CSSResultGroup {
|
||||
// return unsafeCSS(galleryStyle);
|
||||
// }
|
||||
// }
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-gallery-core': FrigateCardGalleryCore;
|
||||
//'frigate-card-gallery-core': FrigateCardGalleryCore;
|
||||
'frigate-card-gallery': FrigateCardGallery;
|
||||
}
|
||||
}
|
||||
|
||||
+13
-22
@@ -33,7 +33,6 @@ import {
|
||||
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
||||
import { contentsChanged } from '../../utils/basic.js';
|
||||
import { getCameraIcon, getCameraTitle } from '../../utils/camera.js';
|
||||
import { getFullDependentBrowseMediaQueryParameters } from '../../utils/ha/browse-media.js';
|
||||
import {
|
||||
dispatchExistingMediaLoadedInfoAsEvent,
|
||||
dispatchMediaUnloadedEvent,
|
||||
@@ -52,7 +51,7 @@ import '../surround.js';
|
||||
import { EmblaCarouselPlugins } from '../carousel.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
||||
import { DataManager } from '../../utils/data-manager.js';
|
||||
import { DataManager } from '../../utils/data/data-manager.js';
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { dispatchMessageEvent, dispatchErrorMessageEvent } from '../message.js';
|
||||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
@@ -212,16 +211,6 @@ export class FrigateCardLive extends LitElement {
|
||||
this.conditionState,
|
||||
) as LiveConfig;
|
||||
|
||||
// Does not use getFullDependentBrowseMediaQueryParametersOrDispatchError to
|
||||
// ensure that non-Frigate cameras will work in live view (they will not
|
||||
// have a Frigate camera name).
|
||||
const browseMediaParams = getFullDependentBrowseMediaQueryParameters(
|
||||
this.hass,
|
||||
this.cameras,
|
||||
this.view.camera,
|
||||
config.controls.thumbnails.media,
|
||||
);
|
||||
|
||||
// Notes:
|
||||
// - See use of liveConfig and not config below -- the carousel will
|
||||
// independently override the liveConfig to reflect the camera in the
|
||||
@@ -238,10 +227,9 @@ export class FrigateCardLive extends LitElement {
|
||||
html`<frigate-card-surround
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.fetch=${true}
|
||||
.fetchMedia=${config.controls.thumbnails.media}
|
||||
.thumbnailConfig=${config.controls.thumbnails}
|
||||
.timelineConfig=${config.controls.timeline}
|
||||
.browseMediaParams=${browseMediaParams ?? undefined}
|
||||
.cameras=${this.cameras}
|
||||
.dataManager=${this.dataManager}
|
||||
.inBackground=${this._inBackground}
|
||||
@@ -375,16 +363,19 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
protected _getSelectedCameraIndex(): number {
|
||||
if (!this.cameras || !this.view) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, Array.from(this.cameras.keys()).indexOf(this.view.camera));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Embla options to use.
|
||||
* @returns An EmblaOptionsType object or undefined for no options.
|
||||
*/
|
||||
protected _getOptions(): EmblaOptionsType {
|
||||
return {
|
||||
startIndex:
|
||||
this.cameras && this.view
|
||||
? Math.max(0, Array.from(this.cameras.keys()).indexOf(this.view.camera))
|
||||
: 0,
|
||||
draggable: this.liveConfig?.draggable,
|
||||
loop: true,
|
||||
};
|
||||
@@ -483,10 +474,9 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
this.view
|
||||
.evolve({
|
||||
camera: Array.from(this.cameras.keys())[selectedCameraIndex],
|
||||
|
||||
// Reset the target.
|
||||
target: null,
|
||||
childIndex: null,
|
||||
// Reset the query and query results.
|
||||
query: null,
|
||||
queryResults: null,
|
||||
})
|
||||
// Don't yet fetch thumbnails (they will be fetched when the carousel
|
||||
// settles).
|
||||
@@ -624,6 +614,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
) as EmblaCarouselPlugins}
|
||||
.label="${title ? `${localize('common.live')}: ${title}` : ''}"
|
||||
.titlePopupConfig=${config.controls.title}
|
||||
.selected=${this._getSelectedCameraIndex()}
|
||||
transitionEffect=${this._getTransitionEffect()}
|
||||
@frigate-card:media-carousel:select=${this._setViewHandler.bind(this)}
|
||||
@frigate-card:carousel:settle=${() => {
|
||||
|
||||
@@ -126,6 +126,9 @@ export class FrigateCardMediaCarousel extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public carouselPlugins?: EmblaCarouselPlugins;
|
||||
|
||||
@property({ attribute: false, type: Number })
|
||||
public selected = 0;
|
||||
|
||||
@property({ attribute: true })
|
||||
public transitionEffect?: TransitionEffect;
|
||||
|
||||
@@ -418,6 +421,7 @@ export class FrigateCardMediaCarousel extends LitElement {
|
||||
|
||||
return html` <frigate-card-carousel
|
||||
${ref(this._refCarousel)}
|
||||
.selected=${this.selected ?? 0}
|
||||
.carouselOptions=${this.carouselOptions}
|
||||
.carouselPlugins=${this.carouselPlugins}
|
||||
transitionEffect=${ifDefined(this.transitionEffect)}
|
||||
|
||||
@@ -177,9 +177,11 @@ export function dispatchErrorMessageEvent(
|
||||
*/
|
||||
export function dispatchFrigateCardErrorEvent(
|
||||
element: EventTarget,
|
||||
error: FrigateCardError,
|
||||
error: FrigateCardError | Error,
|
||||
): void {
|
||||
dispatchErrorMessageEvent(element, error.message, { context: error.context });
|
||||
dispatchErrorMessageEvent(element, error.message, {
|
||||
...(error instanceof FrigateCardError && { context: error.context }),
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
+28
-44
@@ -7,29 +7,20 @@ import {
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
import surroundStyle from '../scss/surround.scss';
|
||||
import {
|
||||
BrowseMediaQueryParameters,
|
||||
CameraConfig,
|
||||
ClipsOrSnapshotsOrAll,
|
||||
ExtendedHomeAssistant,
|
||||
FrigateBrowseMediaSource,
|
||||
FrigateCardError,
|
||||
MiniTimelineControlConfig,
|
||||
ThumbnailsControlConfig,
|
||||
} from '../types.js';
|
||||
import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||
import {
|
||||
getFirstTrueMediaChildIndex,
|
||||
multipleBrowseMediaQueryMerged,
|
||||
} from '../utils/ha/browse-media';
|
||||
import { DataManager } from '../utils/data-manager';
|
||||
import { DataManager } from '../utils/data/data-manager.js';
|
||||
import { View } from '../view.js';
|
||||
import { dispatchFrigateCardErrorEvent } from './message.js';
|
||||
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
||||
|
||||
import './surround-basic.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { changeViewToRecentEventsForCameraAndDependents } from '../utils/media-to-view';
|
||||
|
||||
interface ThumbnailViewContext {
|
||||
// Whether or not to fetch thumbnails.
|
||||
@@ -59,11 +50,9 @@ export class FrigateCardSurround extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public inBackground?: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
public fetch = false;
|
||||
|
||||
// If fetchMedia is not specified, no fetching is done.
|
||||
@property({ attribute: false, hasChanged: contentsChanged })
|
||||
public browseMediaParams?: BrowseMediaQueryParameters | BrowseMediaQueryParameters[];
|
||||
public fetchMedia?: ClipsOrSnapshotsOrAll;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameras?: Map<string, CameraConfig>;
|
||||
@@ -79,32 +68,29 @@ export class FrigateCardSurround extends LitElement {
|
||||
*/
|
||||
protected async _fetchMedia(): Promise<void> {
|
||||
if (
|
||||
!this.fetch ||
|
||||
!this.cameras ||
|
||||
!this.dataManager ||
|
||||
!this.fetchMedia ||
|
||||
this.inBackground ||
|
||||
!this.hass ||
|
||||
!this.view ||
|
||||
this.view.target ||
|
||||
this.view.query ||
|
||||
!this.thumbnailConfig ||
|
||||
this.thumbnailConfig.mode === 'none' ||
|
||||
!this.browseMediaParams ||
|
||||
!(this.view.context?.thumbnails?.fetch ?? true)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let parent: FrigateBrowseMediaSource | null;
|
||||
try {
|
||||
parent = await multipleBrowseMediaQueryMerged(this.hass, this.browseMediaParams);
|
||||
} catch (e) {
|
||||
return dispatchFrigateCardErrorEvent(this, e as FrigateCardError);
|
||||
}
|
||||
if (getFirstTrueMediaChildIndex(parent) !== null) {
|
||||
this.view
|
||||
?.evolve({
|
||||
target: parent,
|
||||
childIndex: null,
|
||||
})
|
||||
.dispatchChangeEvent(this);
|
||||
}
|
||||
await changeViewToRecentEventsForCameraAndDependents(
|
||||
this,
|
||||
this.hass,
|
||||
this.dataManager,
|
||||
this.cameras,
|
||||
this.view,
|
||||
{
|
||||
mediaType: this.fetchMedia,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,25 +156,21 @@ export class FrigateCardSurround extends LitElement {
|
||||
slot=${this.thumbnailConfig.mode}
|
||||
.hass=${this.hass}
|
||||
.config=${this.thumbnailConfig}
|
||||
.dataManager=${this.dataManager}
|
||||
.view=${this.view}
|
||||
.target=${this.view.target}
|
||||
.cameras=${this.cameras}
|
||||
selected=${ifDefined(this.view.childIndex ?? undefined)}
|
||||
.selected=${this.view.queryResults?.getSelectedIndex() ?? undefined}
|
||||
@frigate-card:view:change=${(ev: CustomEvent) => changeDrawer(ev, 'close')}
|
||||
@frigate-card:thumbnail-carousel:tap=${(
|
||||
ev: CustomEvent<ThumbnailCarouselTap>,
|
||||
) => {
|
||||
const child: FrigateBrowseMediaSource | null =
|
||||
ev.detail.target?.children?.[ev.detail.childIndex] ?? null;
|
||||
if (child) {
|
||||
const media = ev.detail.queryResults.getSelectedResult();
|
||||
if (media) {
|
||||
this.view
|
||||
?.evolve({
|
||||
view: this.view.is('recording') ? 'recording' : 'media',
|
||||
target: ev.detail.target,
|
||||
childIndex: ev.detail.childIndex,
|
||||
...(child.frigate?.cameraID && {
|
||||
camera: child.frigate?.cameraID,
|
||||
}),
|
||||
queryResults: ev.detail.queryResults,
|
||||
...(media.getCameraID() && { camera: media.getCameraID() }),
|
||||
})
|
||||
.removeContext('timeline')
|
||||
// Send the view change from the source of the tap event, so
|
||||
@@ -200,7 +182,9 @@ export class FrigateCardSurround extends LitElement {
|
||||
>
|
||||
</frigate-card-thumbnail-carousel>`
|
||||
: ''}
|
||||
${this.timelineConfig?.mode && this.timelineConfig.mode !== 'none' && !this.inBackground
|
||||
${this.timelineConfig?.mode &&
|
||||
this.timelineConfig.mode !== 'none' &&
|
||||
!this.inBackground
|
||||
? html` <frigate-card-timeline-core
|
||||
slot=${this.timelineConfig.mode}
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -15,22 +15,19 @@ import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss';
|
||||
import {
|
||||
CameraConfig,
|
||||
ExtendedHomeAssistant,
|
||||
FrigateBrowseMediaSource,
|
||||
ThumbnailsControlConfig,
|
||||
} from '../types.js';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||
import { isTrueMedia } from '../utils/ha/browse-media';
|
||||
import { View } from '../view.js';
|
||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||
import { MediaQueriesResults, View } from '../view.js';
|
||||
import { FrigateCardCarousel } from './carousel.js';
|
||||
import './thumbnail.js';
|
||||
import './carousel.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { DataManager } from '../utils/data/data-manager.js';
|
||||
|
||||
export interface ThumbnailCarouselTap {
|
||||
slideIndex: number;
|
||||
target: FrigateBrowseMediaSource;
|
||||
childIndex: number;
|
||||
queryResults: MediaQueriesResults;
|
||||
}
|
||||
|
||||
@customElement('frigate-card-thumbnail-carousel')
|
||||
@@ -41,14 +38,12 @@ export class FrigateCardThumbnailCarousel extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public view?: Readonly<View>;
|
||||
|
||||
// Use contentsChanged here to avoid the carousel rebuilding and resetting in
|
||||
// front of the user, unless the contents have actually changed.
|
||||
@property({ attribute: false, hasChanged: contentsChanged })
|
||||
public target?: FrigateBrowseMediaSource | null;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameras?: Map<string, CameraConfig>;
|
||||
|
||||
@property({ attribute: false })
|
||||
public dataManager?: DataManager;
|
||||
|
||||
protected _refCarousel: Ref<FrigateCardCarousel> = createRef();
|
||||
|
||||
// Thumbnail carousels can expand (e.g. drawer-based carousels after the main
|
||||
@@ -59,10 +54,14 @@ export class FrigateCardThumbnailCarousel extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public config?: ThumbnailsControlConfig;
|
||||
|
||||
@property({ attribute: true, type: Number, reflect: true })
|
||||
public selected?: number;
|
||||
@property({ attribute: false })
|
||||
public selected? = 0;
|
||||
|
||||
protected _carouselOptions?: EmblaOptionsType = {
|
||||
containScroll: 'keepSnaps',
|
||||
dragFree: true,
|
||||
};
|
||||
|
||||
protected _carouselOptions?: EmblaOptionsType;
|
||||
protected _carouselPlugins: EmblaPluginType[] = [
|
||||
WheelGesturesPlugin({
|
||||
// Whether the carousel is vertical or horizontal, interpret y-axis wheel
|
||||
@@ -99,31 +98,20 @@ export class FrigateCardThumbnailCarousel extends LitElement {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Embla options to use.
|
||||
* @returns An EmblaOptionsType object or undefined for no options.
|
||||
*/
|
||||
protected _getOptions(): EmblaOptionsType {
|
||||
return {
|
||||
containScroll: 'keepSnaps',
|
||||
dragFree: true,
|
||||
startIndex: this.selected ?? 0,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get slides to include in the render.
|
||||
* @returns The slides to include in the render.
|
||||
*/
|
||||
protected _getSlides(): TemplateResult[] {
|
||||
if (!this.target || !this.target.children || !this.target.children.length) {
|
||||
if (!this.view?.query || !this.view.queryResults?.hasResults()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const slides: TemplateResult[] = [];
|
||||
for (let i = 0; i < this.target.children.length; ++i) {
|
||||
const thumbnail = this._renderThumbnail(this.target, i, slides.length);
|
||||
for (let i = 0; i < this.view.queryResults.getResultsCount(); ++i) {
|
||||
const thumbnail = this._renderThumbnail(i);
|
||||
if (thumbnail) {
|
||||
slides.push(thumbnail);
|
||||
slides[i] = thumbnail;
|
||||
}
|
||||
}
|
||||
return slides;
|
||||
@@ -152,30 +140,6 @@ export class FrigateCardThumbnailCarousel extends LitElement {
|
||||
this.selected === undefined ? '1.0' : '0.4',
|
||||
);
|
||||
}
|
||||
|
||||
if (!this._carouselOptions) {
|
||||
// Want to set the initial carousel options just before the first render
|
||||
// in order to get the startIndex correct in the options. It is not safe
|
||||
// to rely on carouselScrollTo() post update, since the nested carousel
|
||||
// may not yet be actual rendered/created.
|
||||
this._carouselOptions = this._getOptions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The updated lifecycle callback for this element.
|
||||
* @param changedProperties The properties that were changed in this render.
|
||||
*/
|
||||
updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has('selected')) {
|
||||
this.updateComplete.then(() => {
|
||||
if (this.selected !== undefined) {
|
||||
this._refCarousel.value?.carouselScrollTo(this.selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,45 +147,40 @@ export class FrigateCardThumbnailCarousel extends LitElement {
|
||||
* @param mediaToRender The media item to render.
|
||||
* @returns A template or void if the item could not be rendered.
|
||||
*/
|
||||
protected _renderThumbnail(
|
||||
parent: FrigateBrowseMediaSource,
|
||||
childIndex: number,
|
||||
slideIndex: number,
|
||||
): TemplateResult | void {
|
||||
if (
|
||||
!parent.children ||
|
||||
!parent.children.length ||
|
||||
!isTrueMedia(parent.children[childIndex])
|
||||
) {
|
||||
protected _renderThumbnail(index: number): TemplateResult | void {
|
||||
const media = this.view?.queryResults?.getResult(index) ?? null;
|
||||
const cameraConfig = media ? this.cameras?.get(media.getCameraID()) : null;
|
||||
if (!media || !cameraConfig || !this.view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const classes = {
|
||||
embla__slide: true,
|
||||
'slide-selected': this.selected === childIndex,
|
||||
'slide-selected': this.selected === index,
|
||||
};
|
||||
|
||||
const cameraConfig = this.view?.camera ? this.cameras?.get(this.view.camera) : null;
|
||||
return html` <frigate-card-thumbnail
|
||||
class="${classMap(classes)}"
|
||||
.dataManager=${this.dataManager}
|
||||
.hass=${this.hass}
|
||||
.media=${media}
|
||||
.cameraConfig=${cameraConfig}
|
||||
.view=${this.view}
|
||||
.target=${parent}
|
||||
.childIndex=${childIndex}
|
||||
.mediaSeek=${this.view?.context?.mediaViewer?.seek.get(childIndex)}
|
||||
.cameraConfig=${cameraConfig ?? undefined}
|
||||
?details=${this.config?.show_details}
|
||||
.mediaSeek=${this.view?.context?.mediaViewer?.seek.get(index)}
|
||||
?details=${!!this.config?.show_details}
|
||||
?show_favorite_control=${this.config?.show_favorite_control}
|
||||
?show_timeline_control=${this.config?.show_timeline_control}
|
||||
class="${classMap(classes)}"
|
||||
@click=${(ev) => {
|
||||
if (this._refCarousel.value?.carouselClickAllowed()) {
|
||||
@click=${(ev: Event) => {
|
||||
if (
|
||||
this.view &&
|
||||
this.view.queryResults &&
|
||||
this._refCarousel.value?.carouselClickAllowed()
|
||||
) {
|
||||
dispatchFrigateCardEvent<ThumbnailCarouselTap>(
|
||||
this,
|
||||
'thumbnail-carousel:tap',
|
||||
{
|
||||
slideIndex: slideIndex,
|
||||
target: parent,
|
||||
childIndex: childIndex,
|
||||
queryResults: this.view.queryResults.clone().selectResult(index),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -257,6 +216,7 @@ export class FrigateCardThumbnailCarousel extends LitElement {
|
||||
return html`<frigate-card-carousel
|
||||
${ref(this._refCarousel)}
|
||||
direction=${ifDefined(this._getDirection())}
|
||||
.selected=${this.selected ?? 0}
|
||||
.carouselOptions=${this._carouselOptions}
|
||||
.carouselPlugins=${this._carouselPlugins}
|
||||
>
|
||||
|
||||
+111
-121
@@ -3,30 +3,24 @@ import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
import { CSSResult, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
|
||||
import { localize } from '../localize/localize.js';
|
||||
import thumbnailDetailsStyle from '../scss/thumbnail-details.scss';
|
||||
import thumbnailFeatureEventStyle from '../scss/thumbnail-feature-event.scss';
|
||||
import thumbnailFeatureRecordingStyle from '../scss/thumbnail-feature-recording.scss';
|
||||
import thumbnailStyle from '../scss/thumbnail.scss';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
import { errorToConsole, prettifyTitle } from '../utils/basic.js';
|
||||
import { getDurationString, prettifyTitle } from '../utils/basic.js';
|
||||
import { getCameraTitle } from '../utils/camera.js';
|
||||
import { retainEvent } from '../utils/frigate.js';
|
||||
import { getEventDurationString } from '../utils/frigate.js';
|
||||
import { renderTask } from '../utils/task.js';
|
||||
import { createFetchThumbnailTask } from '../utils/thumbnail.js';
|
||||
import { View } from '../view.js';
|
||||
import type { MediaSeek } from './viewer.js';
|
||||
import { TaskStatus } from '@lit-labs/task';
|
||||
|
||||
import type {
|
||||
CameraConfig,
|
||||
ExtendedHomeAssistant,
|
||||
FrigateBrowseMediaSource,
|
||||
FrigateEvent,
|
||||
FrigateRecording,
|
||||
} from '../types.js';
|
||||
import type { CameraConfig, ExtendedHomeAssistant } from '../types.js';
|
||||
import { ViewMedia } from '../view-media.js';
|
||||
import { DataManager } from '../utils/data/data-manager.js';
|
||||
|
||||
// The minimum width of a thumbnail with details enabled.
|
||||
export const THUMBNAIL_DETAILS_WIDTH_MIN = 300;
|
||||
|
||||
@@ -133,26 +127,36 @@ export class FrigateCardThumbnailFeatureRecording extends LitElement {
|
||||
@customElement('frigate-card-thumbnail-details-event')
|
||||
export class FrigateCardThumbnailDetailsEvent extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public event?: FrigateEvent;
|
||||
public media?: ViewMedia;
|
||||
|
||||
@property({ attribute: false })
|
||||
public mediaSeek?: MediaSeek;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.event) {
|
||||
if (!this.media || !this.media.isEvent()) {
|
||||
return;
|
||||
}
|
||||
const score = (this.event.top_score * 100).toFixed(2) + '%';
|
||||
return html`<div class="left">
|
||||
<div class="larger">${prettifyTitle(this.event.label)}</div>
|
||||
<div>
|
||||
<span class="heading">${localize('event.start')}:</span>
|
||||
<span>${format(fromUnixTime(this.event.start_time), 'HH:mm:ss')}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="heading">${localize('event.duration')}:</span>
|
||||
<span>${getEventDurationString(this.event)}</span>
|
||||
</div>
|
||||
const score = this.media.getScore();
|
||||
const startTime = this.media.getStartTime();
|
||||
const endTime = this.media.getEndTime();
|
||||
const what = this.media.getWhat();
|
||||
|
||||
return html` <div class="left">
|
||||
${what ? html`<div class="larger">${prettifyTitle(what.join(', '))}</div>` : ``}
|
||||
${startTime
|
||||
? html` <div>
|
||||
<span class="heading">${localize('event.start')}:</span>
|
||||
<span>${format(startTime, 'HH:mm:ss')}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="heading">${localize('event.duration')}:</span>
|
||||
<span
|
||||
>${endTime
|
||||
? getDurationString(startTime, endTime)
|
||||
: localize('event.in_progress')}</span
|
||||
>
|
||||
</div>`
|
||||
: ``}
|
||||
${this.mediaSeek
|
||||
? html` <div>
|
||||
<span class="heading">${localize('event.seek')}</span>
|
||||
@@ -160,9 +164,11 @@ export class FrigateCardThumbnailDetailsEvent extends LitElement {
|
||||
</div>`
|
||||
: html``}
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="larger">${score}</span>
|
||||
</div>`;
|
||||
${score
|
||||
? html`<div class="right">
|
||||
<span class="larger">${(score * 100).toFixed(2) + '%'}</span>
|
||||
</div>`
|
||||
: ``}`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
@@ -173,17 +179,21 @@ export class FrigateCardThumbnailDetailsEvent extends LitElement {
|
||||
@customElement('frigate-card-thumbnail-details-recording')
|
||||
export class FrigateCardThumbnailDetailsRecording extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public recording?: FrigateRecording;
|
||||
public media?: ViewMedia;
|
||||
|
||||
@property({ attribute: false })
|
||||
public mediaSeek?: MediaSeek;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraTitle?: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.recording) {
|
||||
if (!this.media) {
|
||||
return;
|
||||
}
|
||||
const eventCount = this.media.getEventCount();
|
||||
return html`<div class="left">
|
||||
<div class="larger">${prettifyTitle(this.recording.camera) || ''}</div>
|
||||
<div class="larger">${this.cameraTitle ?? ''}</div>
|
||||
${this.mediaSeek
|
||||
? html` <div>
|
||||
<span class="heading">${localize('recording.seek')}</span>
|
||||
@@ -191,10 +201,12 @@ export class FrigateCardThumbnailDetailsRecording extends LitElement {
|
||||
</div>`
|
||||
: html``}
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="larger">${this.recording.events}</span>
|
||||
<span>${localize('recording.events')}</span>
|
||||
</div>`;
|
||||
${eventCount !== null
|
||||
? html`<div class="right">
|
||||
<span class="larger">${eventCount}</span>
|
||||
<span>${localize('recording.events')}</span>
|
||||
</div>`
|
||||
: ``}`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
@@ -204,6 +216,21 @@ export class FrigateCardThumbnailDetailsRecording extends LitElement {
|
||||
|
||||
@customElement('frigate-card-thumbnail')
|
||||
export class FrigateCardThumbnail extends LitElement {
|
||||
// HomeAssistant object may be required for thumbnail signing (for Frigate
|
||||
// events).
|
||||
@property({ attribute: false })
|
||||
public hass?: ExtendedHomeAssistant;
|
||||
|
||||
// DataManager used for marking media as favorite.
|
||||
@property({ attribute: false })
|
||||
public dataManager?: DataManager;
|
||||
|
||||
@property({ attribute: true })
|
||||
public media?: ViewMedia;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraConfig?: CameraConfig;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public details = false;
|
||||
|
||||
@@ -213,160 +240,123 @@ export class FrigateCardThumbnail extends LitElement {
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public show_timeline_control = false;
|
||||
|
||||
// ======================
|
||||
// Target-based interface
|
||||
// ======================
|
||||
@property({ attribute: false })
|
||||
public target?: FrigateBrowseMediaSource | null;
|
||||
|
||||
@property({ attribute: false })
|
||||
public childIndex?: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
public mediaSeek?: MediaSeek;
|
||||
|
||||
// ===================================================
|
||||
// Raw interface (can override target-based interface)
|
||||
// ===================================================
|
||||
@property({ attribute: true })
|
||||
public thumbnail?: string;
|
||||
|
||||
@property({ attribute: true })
|
||||
public label?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public event?: FrigateEvent;
|
||||
|
||||
// ================================
|
||||
// Optional parameters for controls
|
||||
// ================================
|
||||
@property({ attribute: false })
|
||||
public view?: Readonly<View>;
|
||||
|
||||
@property({ attribute: false })
|
||||
public hass?: ExtendedHomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraConfig?: CameraConfig;
|
||||
|
||||
/**
|
||||
* Render the element.
|
||||
* @returns A template to display to the user.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
let event: FrigateEvent | null = null;
|
||||
let recording: FrigateRecording | null = null;
|
||||
let thumbnail: string | null = null;
|
||||
let label: string | null = null;
|
||||
|
||||
// Take the event / thumbnail / label from the data-bound media (if specified).
|
||||
if (this.target && this.target.children && this.childIndex !== undefined) {
|
||||
const media = this.target.children[this.childIndex];
|
||||
event = media.frigate?.event ?? null;
|
||||
recording = media.frigate?.recording ?? null;
|
||||
thumbnail = media.thumbnail;
|
||||
label = media.title;
|
||||
}
|
||||
|
||||
// Always give the overrides preference (if specified).
|
||||
if (this.event) {
|
||||
event = this.event;
|
||||
}
|
||||
thumbnail = this.thumbnail ? this.thumbnail : thumbnail;
|
||||
label = this.label ? this.label : label;
|
||||
|
||||
if (!event && !recording) {
|
||||
if (!this.media || !this.cameraConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const thumbnail = this.media.getThumbnail(this.cameraConfig);
|
||||
const title = this.media.getTitle(this.cameraConfig) ?? '';
|
||||
|
||||
const starClasses = {
|
||||
star: true,
|
||||
starred: !!event?.retain_indefinitely,
|
||||
starred: !!this.media?.isFavorite(),
|
||||
};
|
||||
|
||||
const shouldShowTimelineControl =
|
||||
this.show_timeline_control &&
|
||||
this.view &&
|
||||
(!this.media.isRecording() ||
|
||||
// Only show timeline control if the recording has a start & end time.
|
||||
(this.media.getStartTime() && this.media.getEndTime()));
|
||||
|
||||
const clientID = this.cameraConfig?.frigate.client_id;
|
||||
return html` ${event
|
||||
return html` ${this.media.isEvent()
|
||||
? html`<frigate-card-thumbnail-feature-event
|
||||
aria-label="${label ?? ''}"
|
||||
title="${label ?? ''}"
|
||||
aria-label="${title ?? ''}"
|
||||
title=${title}
|
||||
.hass=${this.hass}
|
||||
.thumbnail=${thumbnail ?? undefined}
|
||||
.label=${label ?? undefined}
|
||||
></frigate-card-thumbnail-feature-event>`
|
||||
: recording
|
||||
: this.media.isRecording()
|
||||
? html`<frigate-card-thumbnail-feature-recording
|
||||
aria-label="${label ?? ''}"
|
||||
title="${label ?? ''}"
|
||||
aria-label="${title ?? ''}"
|
||||
title="${title ?? ''}"
|
||||
.cameraTitle=${this.details || !this.cameraConfig || !this.hass
|
||||
? undefined
|
||||
: getCameraTitle(this.hass, this.cameraConfig)}
|
||||
.date=${recording ? fromUnixTime(recording.start_time) : undefined}
|
||||
.date=${this.media.getStartTime() ?? undefined}
|
||||
></frigate-card-thumbnail-feature-recording>`
|
||||
: html``}
|
||||
${this.show_favorite_control && event && this.hass && clientID
|
||||
? html` <ha-icon
|
||||
class="${classMap(starClasses)}"
|
||||
icon=${event?.retain_indefinitely ? 'mdi:star' : 'mdi:star-outline'}
|
||||
icon=${this.media.isFavorite() ? 'mdi:star' : 'mdi:star-outline'}
|
||||
title=${localize('thumbnail.retain_indefinitely')}
|
||||
@click=${(ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
if (event && this.hass && clientID) {
|
||||
retainEvent(this.hass, clientID, event.id, !event.retain_indefinitely)
|
||||
.then(() => {
|
||||
if (event) {
|
||||
event.retain_indefinitely = !event.retain_indefinitely;
|
||||
this.requestUpdate();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
errorToConsole(e);
|
||||
});
|
||||
if (this.hass && this.cameraConfig && this.media) {
|
||||
this.dataManager?.favoriteMedia(
|
||||
this.hass,
|
||||
this.cameraConfig,
|
||||
this.media,
|
||||
!this.media?.isFavorite(),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/></ha-icon>`
|
||||
: ``}
|
||||
${this.details && event
|
||||
${this.details && this.media.isEvent()
|
||||
? html`<frigate-card-thumbnail-details-event
|
||||
.event=${event ?? undefined}
|
||||
.media=${this.media ?? undefined}
|
||||
.mediaSeek=${this.mediaSeek}
|
||||
></frigate-card-thumbnail-details-event>`
|
||||
: this.details && recording
|
||||
: this.details && this.media.isRecording()
|
||||
? html`<frigate-card-thumbnail-details-recording
|
||||
.recording=${recording ?? undefined}
|
||||
.media=${this.media ?? undefined}
|
||||
.cameraTitle=${getCameraTitle(this.hass, this.cameraConfig)}
|
||||
.mediaSeek=${this.mediaSeek}
|
||||
></frigate-card-thumbnail-details-recording>`
|
||||
: html``}
|
||||
${this.show_timeline_control
|
||||
${shouldShowTimelineControl
|
||||
? html`<ha-icon
|
||||
class="timeline"
|
||||
icon="mdi:target"
|
||||
title=${localize('thumbnail.timeline')}
|
||||
@click=${(ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
if (event) {
|
||||
if (!this.view || !this.media) {
|
||||
return;
|
||||
}
|
||||
if (this.media.isEvent()) {
|
||||
this.view
|
||||
?.evolve({
|
||||
.evolve({
|
||||
view: 'timeline',
|
||||
target: this.target,
|
||||
childIndex: this.childIndex ?? null,
|
||||
queryResults: this.view.queryResults
|
||||
?.clone()
|
||||
.selectResultIfFound((media) => media === this.media),
|
||||
})
|
||||
.removeContext('timeline')
|
||||
.dispatchChangeEvent(this);
|
||||
} else if (recording) {
|
||||
} else if (this.media.isRecording()) {
|
||||
const startTime = this.media.getStartTime();
|
||||
const endTime = this.media.getStartTime();
|
||||
if (!startTime || !endTime) {
|
||||
return;
|
||||
}
|
||||
// Specifically reset the media target/childIndex, as we cannot
|
||||
// 'select' an hour in the timeline rather we set the window to
|
||||
// matching values.
|
||||
this.view
|
||||
?.evolve({
|
||||
view: 'timeline',
|
||||
target: null,
|
||||
childIndex: null,
|
||||
query: null,
|
||||
})
|
||||
.mergeInContext({
|
||||
timeline: {
|
||||
window: {
|
||||
start: fromUnixTime(recording.start_time),
|
||||
end: fromUnixTime(recording.end_time),
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
+480
-441
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import timelineStyle from '../scss/timeline.scss';
|
||||
import { CameraConfig, ExtendedHomeAssistant, TimelineConfig } from '../types';
|
||||
import { DataManager } from '../utils/data-manager';
|
||||
import { DataManager } from '../utils/data/data-manager';
|
||||
import { View } from '../view';
|
||||
import './surround.js';
|
||||
import './timeline-core.js';
|
||||
@@ -43,7 +43,6 @@ export class FrigateCardTimeline extends LitElement {
|
||||
.view=${this.view}
|
||||
.thumbnailConfig=${this.timelineConfig.controls.thumbnails}
|
||||
.cameras=${this.cameras}
|
||||
.fetch=${false}
|
||||
>
|
||||
<frigate-card-timeline-core
|
||||
.hass=${this.hass}
|
||||
|
||||
+187
-304
@@ -16,30 +16,21 @@ import { renderProgressIndicator } from '../components/message.js';
|
||||
import viewerStyle from '../scss/viewer.scss';
|
||||
import viewerCarouselStyle from '../scss/viewer-carousel.scss';
|
||||
import {
|
||||
BrowseMediaNeighbors,
|
||||
BrowseMediaQueryParameters,
|
||||
CameraConfig,
|
||||
CardWideConfig,
|
||||
ExtendedHomeAssistant,
|
||||
FrigateBrowseMediaSource,
|
||||
frigateCardConfigDefaults,
|
||||
FrigateCardMediaPlayer,
|
||||
MediaLoadedInfo,
|
||||
ResolvedMedia,
|
||||
TransitionEffect,
|
||||
ViewerConfig,
|
||||
} from '../types.js';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
import { contentsChanged } from '../utils/basic.js';
|
||||
import {
|
||||
fetchLatestMediaAndDispatchViewChange,
|
||||
getEventStartTime,
|
||||
getFullDependentBrowseMediaQueryParametersOrDispatchError,
|
||||
isTrueMedia,
|
||||
multipleBrowseMediaQueryMerged,
|
||||
overrideMultiBrowseMediaQueryParameters,
|
||||
} from '../utils/ha/browse-media.js';
|
||||
import { getFullDependentBrowseMediaQueryParametersOrDispatchError } from '../utils/ha/browse-media.js';
|
||||
import { ResolvedMediaCache, resolveMedia } from '../utils/ha/resolved-media.js';
|
||||
import { View } from '../view.js';
|
||||
import { MediaQueriesResults, View } from '../view.js';
|
||||
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
||||
import { Lazyload } from './embla-plugins/lazyload.js';
|
||||
import {
|
||||
@@ -55,8 +46,13 @@ import '../patches/ha-hls-player';
|
||||
import './surround.js';
|
||||
import { renderTask } from '../utils/task.js';
|
||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
||||
import { DataManager } from '../utils/data-manager.js';
|
||||
import { changeViewToRecentRecordingForCameraAndDependents } from '../utils/media-to-view.js';
|
||||
import { DataManager } from '../utils/data/data-manager.js';
|
||||
import {
|
||||
changeViewToRecentEventsForCameraAndDependents,
|
||||
changeViewToRecentRecordingForCameraAndDependents,
|
||||
} from '../utils/media-to-view.js';
|
||||
import { ViewMedia, ViewMediaClassifier } from '../view-media.js';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
|
||||
export interface MediaSeek {
|
||||
// Specifies the point at which this recording should be played, the
|
||||
@@ -123,10 +119,10 @@ export class FrigateCardViewer extends LitElement {
|
||||
this.view.camera,
|
||||
);
|
||||
|
||||
if (!this.view.target) {
|
||||
// If the target is not specified, the view must tell us which mediaType
|
||||
// to search for. When the target *is* specified, the view is not required
|
||||
// to indicate the media type (e.g. the mixed 'events' view from the
|
||||
if (!this.view.queryResults?.hasResults()) {
|
||||
// If the query is not specified, the view must tell us which mediaType to
|
||||
// search for. When the query *is* specified, the view is not required to
|
||||
// indicate the media type (e.g. the mixed 'media' view from the
|
||||
// timeline).
|
||||
const mediaType = this.view.getMediaType();
|
||||
if (!browseMediaQueryParameters || !mediaType) {
|
||||
@@ -145,13 +141,15 @@ export class FrigateCardViewer extends LitElement {
|
||||
},
|
||||
);
|
||||
} else {
|
||||
fetchLatestMediaAndDispatchViewChange(
|
||||
changeViewToRecentEventsForCameraAndDependents(
|
||||
this,
|
||||
this.hass,
|
||||
this.dataManager,
|
||||
this.cameras,
|
||||
this.view,
|
||||
overrideMultiBrowseMediaQueryParameters(browseMediaQueryParameters, {
|
||||
mediaType: mediaType,
|
||||
}),
|
||||
{
|
||||
targetView: mediaType === 'clips' ? 'clip' : 'snapshot',
|
||||
},
|
||||
);
|
||||
}
|
||||
return renderProgressIndicator({ cardWideConfig: this.cardWideConfig });
|
||||
@@ -160,7 +158,6 @@ export class FrigateCardViewer extends LitElement {
|
||||
return html` <frigate-card-surround
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.fetch=${false}
|
||||
.thumbnailConfig=${this.viewerConfig.controls.thumbnails}
|
||||
.timelineConfig=${this.viewerConfig.controls.timeline}
|
||||
.dataManager=${this.dataManager}
|
||||
@@ -169,8 +166,8 @@ export class FrigateCardViewer extends LitElement {
|
||||
<frigate-card-viewer-carousel
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.cameras=${this.cameras}
|
||||
.viewerConfig=${this.viewerConfig}
|
||||
.browseMediaQueryParameters=${browseMediaQueryParameters}
|
||||
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
@@ -204,46 +201,52 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
@property({ attribute: false, hasChanged: contentsChanged })
|
||||
public viewerConfig?: ViewerConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public browseMediaQueryParameters?: BrowseMediaQueryParameters[] | null;
|
||||
|
||||
@property({ attribute: false })
|
||||
public resolvedMediaCache?: ResolvedMediaCache;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
protected _refMediaCarousel: Ref<FrigateCardMediaCarousel> = createRef();
|
||||
@property({ attribute: false })
|
||||
public cameras?: Map<string, CameraConfig>;
|
||||
|
||||
// Mapping of slide # to FrigateBrowseMediaSource child #.
|
||||
// (Folders are not media items that can be rendered).
|
||||
protected _slideToChild: Record<number, number> = {};
|
||||
protected _refMediaCarousel: Ref<FrigateCardMediaCarousel> = createRef();
|
||||
|
||||
protected _carouselOptions?: EmblaOptionsType;
|
||||
protected _carouselPlugins?: EmblaPluginType[];
|
||||
|
||||
// A task to resolve target media if lazy loading is disabled.
|
||||
protected _mediaResolutionTask = new Task<
|
||||
[FrigateBrowseMediaSource | null | undefined],
|
||||
[ViewerConfig | undefined, Map<string, CameraConfig> | undefined, View | undefined],
|
||||
void
|
||||
>(
|
||||
this,
|
||||
async ([target]: (FrigateBrowseMediaSource | null | undefined)[]): Promise<void> => {
|
||||
for (
|
||||
let i = 0;
|
||||
!this.viewerConfig?.lazy_load &&
|
||||
this.hass &&
|
||||
target &&
|
||||
target.children &&
|
||||
i < (target.children || []).length;
|
||||
++i
|
||||
async ([viewerConfig, cameras, view]: [
|
||||
ViewerConfig | undefined,
|
||||
Map<string, CameraConfig> | undefined,
|
||||
View | undefined,
|
||||
]): Promise<void> => {
|
||||
if (
|
||||
!this.hass ||
|
||||
!viewerConfig?.lazy_load ||
|
||||
!cameras ||
|
||||
!view ||
|
||||
!view.queryResults?.hasResults()
|
||||
) {
|
||||
if (isTrueMedia(target.children[i])) {
|
||||
await resolveMedia(this.hass, target.children[i], this.resolvedMediaCache);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const promises: Promise<ResolvedMedia | null>[] = [];
|
||||
view.queryResults?.getResults()?.forEach((media: ViewMedia) => {
|
||||
const mediaContentID = media.getContentID(cameras.get(media.getCameraID()));
|
||||
if (this.hass && mediaContentID) {
|
||||
promises.push(
|
||||
resolveMedia(this.hass, mediaContentID, this.resolvedMediaCache),
|
||||
);
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
},
|
||||
() => [this.view?.target],
|
||||
() => [this.viewerConfig, this.cameras, this.view],
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -251,27 +254,8 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
* @param changedProperties The properties that were changed in this render.
|
||||
*/
|
||||
updated(changedProperties: PropertyValues): void {
|
||||
const frigateCardCarousel = this._refMediaCarousel.value?.frigateCardCarousel();
|
||||
|
||||
if (frigateCardCarousel && changedProperties.has('view')) {
|
||||
if (changedProperties.has('view')) {
|
||||
const oldView = changedProperties.get('view') as View | undefined;
|
||||
if (oldView) {
|
||||
if (
|
||||
oldView.target === this.view?.target &&
|
||||
oldView.childIndex !== this.view.childIndex
|
||||
) {
|
||||
const slide = this._getSlideForChild(this.view.childIndex);
|
||||
if (
|
||||
slide !== null &&
|
||||
slide !== frigateCardCarousel.getCarouselSelected()?.index
|
||||
) {
|
||||
// If the media target is the same as already loaded, but isn't of
|
||||
// the selected slide, scroll to that slide.
|
||||
frigateCardCarousel.carouselScrollTo(slide);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Seek into the video if the seek time has changed (this is also called
|
||||
// on media load, since the media may or may not have been loaded at
|
||||
// this point).
|
||||
@@ -282,21 +266,6 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
super.updated(changedProperties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the slide number given a media child number.
|
||||
* @param childIndex The child index (relative to `view.target`)
|
||||
* @returns A number or null if the child is not found.
|
||||
*/
|
||||
protected _getSlideForChild(childIndex: number | null | undefined): number | null {
|
||||
if (childIndex === undefined || childIndex === null) {
|
||||
return null;
|
||||
}
|
||||
const slideIndex = Object.keys(this._slideToChild).find(
|
||||
(key) => this._slideToChild[key] === childIndex,
|
||||
);
|
||||
return slideIndex !== undefined ? Number(slideIndex) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the transition effect to use.
|
||||
* @returns An TransitionEffect object.
|
||||
@@ -308,18 +277,6 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Embla options to use.
|
||||
* @returns An EmblaOptionsType object or undefined for no options.
|
||||
*/
|
||||
protected _getOptions(): EmblaOptionsType {
|
||||
return {
|
||||
// Start the carousel on the selected child number.
|
||||
startIndex: this._getSlideForChild(this.view?.childIndex) ?? 0,
|
||||
draggable: this.viewerConfig?.draggable ?? true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The the HLS player on a slide (or current slide if not provided.)
|
||||
* @param slide An optional slide.
|
||||
@@ -344,10 +301,7 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
protected _getPlugins(): EmblaPluginType[] {
|
||||
return [
|
||||
// Only enable wheel plugin if there is more than one media item.
|
||||
...(this.view &&
|
||||
this.view.target &&
|
||||
this.view.target.children &&
|
||||
this.view.target.children.length > 1
|
||||
...(this.view?.queryResults?.getResultsCount() ?? 0 > 1
|
||||
? [
|
||||
WheelGesturesPlugin({
|
||||
// Whether the carousel is vertical or horizontal, interpret y-axis wheel
|
||||
@@ -384,42 +338,20 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
* @returns A BrowseMediaNeighbors with indices and objects of true media
|
||||
* neighbors.
|
||||
*/
|
||||
protected _getMediaNeighbors(): BrowseMediaNeighbors | null {
|
||||
if (
|
||||
!this.view ||
|
||||
!this.view.target ||
|
||||
!this.view.target.children ||
|
||||
this.view.childIndex === null
|
||||
) {
|
||||
return null;
|
||||
protected _getMediaNeighbors(): [ViewMedia | null, ViewMedia | null] {
|
||||
const selectedIndex = this.view?.queryResults?.getSelectedIndex() ?? null;
|
||||
const resultCount = this.view?.queryResults?.getResultsCount() ?? 0;
|
||||
if (!this.view || !this.view.queryResults || selectedIndex === null) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
// Work backwards from the index to get the previous real media.
|
||||
let prevIndex: number | null = null;
|
||||
for (let i = this.view.childIndex - 1; i >= 0; i--) {
|
||||
const media = this.view.target.children[i];
|
||||
if (media && isTrueMedia(media)) {
|
||||
prevIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Work forwards from the index to get the next real media.
|
||||
let nextIndex: number | null = null;
|
||||
for (let i = this.view.childIndex + 1; i < this.view.target.children.length; i++) {
|
||||
const media = this.view.target.children[i];
|
||||
if (media && isTrueMedia(media)) {
|
||||
nextIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
previousIndex: prevIndex,
|
||||
previous: prevIndex != null ? this.view.target.children[prevIndex] : null,
|
||||
nextIndex: nextIndex,
|
||||
next: nextIndex != null ? this.view.target.children[nextIndex] : null,
|
||||
};
|
||||
const previous: ViewMedia | null =
|
||||
selectedIndex > 0 ? this.view.queryResults.getResult(selectedIndex - 1) : null;
|
||||
const next: ViewMedia | null =
|
||||
selectedIndex + 1 < resultCount
|
||||
? this.view.queryResults.getResult(selectedIndex + 1)
|
||||
: null;
|
||||
return [previous, next];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -428,91 +360,55 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
* @param snapshot The snapshot to find a matching clip for.
|
||||
* @returns The view that would show the matching clip.
|
||||
*/
|
||||
protected async _findRelatedClipView(
|
||||
snapshot: FrigateBrowseMediaSource,
|
||||
): Promise<View | null> {
|
||||
protected async _createRelatedClipView(targetIndex: number): Promise<View | null> {
|
||||
const media = this.view?.queryResults?.getResult(targetIndex);
|
||||
|
||||
if (
|
||||
!this.hass ||
|
||||
!this.view ||
|
||||
!this.view.target ||
|
||||
!this.view.target.children ||
|
||||
!this.view.target.children.length ||
|
||||
!this.browseMediaQueryParameters
|
||||
!media ||
|
||||
// If this specific media item has no clip, then do nothing (even if all
|
||||
// the other media items do).
|
||||
!ViewMediaClassifier.isFrigateEvent(media) ||
|
||||
!media.hasClip() ||
|
||||
!this.view.query?.areEventQueries()
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const snapshotStartTime = getEventStartTime(snapshot);
|
||||
if (!snapshotStartTime) {
|
||||
return null;
|
||||
}
|
||||
const newResults: ViewMedia[] = [];
|
||||
let newSelectedIndex: number | null = null;
|
||||
|
||||
// Heuristic: At this point, the user has a particular snapshot that they
|
||||
// are interested in and want to see a related clip, yet the viewer code
|
||||
// does not know the exact search criteria that led to that snapshot (e.g.
|
||||
// it could be a 10-deep folder in the gallery). To give the user to ability
|
||||
// to 'navigate' in the clips view once they change into that mode, this
|
||||
// heuristic finds the earliest and latest snapshot that the user is
|
||||
// currently viewing and mirrors that range into the clips view. Then,
|
||||
// within the results see if there's a clip that matches the same time as
|
||||
// the snapshot.
|
||||
let earliest: number | null = null;
|
||||
let latest: number | null = null;
|
||||
for (let i = 0; i < this.view.target.children.length; i++) {
|
||||
const child = this.view.target.children[i];
|
||||
if (!isTrueMedia(child)) {
|
||||
// Convert the query to a clips equivalent.
|
||||
const newQuery = this.view.query.clone();
|
||||
newQuery.convertToClipsQueries();
|
||||
|
||||
// Regenerate the whole results stack.
|
||||
for (let i = 0; i < (this.view.queryResults?.getResultsCount() ?? 0); ++i) {
|
||||
const media = this.view.queryResults?.getResult(i);
|
||||
if (!media || !ViewMediaClassifier.isFrigateEvent(media)) {
|
||||
continue;
|
||||
}
|
||||
const startTime = getEventStartTime(child);
|
||||
|
||||
if (startTime && (earliest === null || startTime < earliest)) {
|
||||
earliest = startTime;
|
||||
}
|
||||
if (startTime && (latest === null || startTime > latest)) {
|
||||
latest = startTime;
|
||||
const clipMedia = media.getClipEquivalent();
|
||||
if (clipMedia) {
|
||||
newResults.push(clipMedia);
|
||||
if (i === targetIndex) {
|
||||
newSelectedIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!earliest || !latest) {
|
||||
if (newSelectedIndex === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let clips: FrigateBrowseMediaSource | null;
|
||||
const newQueryResults = new MediaQueriesResults(newResults);
|
||||
newQueryResults.selectResult(newSelectedIndex);
|
||||
|
||||
const params = overrideMultiBrowseMediaQueryParameters(
|
||||
this.browseMediaQueryParameters,
|
||||
{
|
||||
mediaType: 'clips',
|
||||
before: latest,
|
||||
after: earliest,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
clips = await multipleBrowseMediaQueryMerged(this.hass, params);
|
||||
} catch (e) {
|
||||
// This is best effort.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!clips || !clips.children || !clips.children.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < clips.children.length; i++) {
|
||||
const child = clips.children[i];
|
||||
if (!isTrueMedia(child)) {
|
||||
continue;
|
||||
}
|
||||
const clipStartTime = getEventStartTime(child);
|
||||
if (clipStartTime && clipStartTime === snapshotStartTime) {
|
||||
return this.view.evolve({
|
||||
view: 'clip',
|
||||
target: clips,
|
||||
childIndex: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return this.view.evolve({
|
||||
view: 'clip',
|
||||
query: newQuery,
|
||||
queryResults: newQueryResults,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -523,13 +419,15 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the childIndex in the view.
|
||||
const childIndex = this._slideToChild[ev.detail.index];
|
||||
if (childIndex !== undefined) {
|
||||
// The slide may already be selected on load, so don't dispatch a new view
|
||||
// unless necessary.
|
||||
if (ev.detail.index !== this.view.queryResults?.getSelectedIndex()) {
|
||||
this.view
|
||||
.evolve({
|
||||
childIndex: childIndex,
|
||||
queryResults: this.view.queryResults?.clone().selectResult(ev.detail.index),
|
||||
})
|
||||
// Ensure the timeline is able to update its position.
|
||||
.mergeInContext({ timeline: { noSetWindow: false } })
|
||||
.dispatchChangeEvent(this);
|
||||
}
|
||||
}
|
||||
@@ -539,11 +437,11 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
* default location will be the Chromecast receiver, not HA).
|
||||
* @param url The media URL
|
||||
*/
|
||||
protected _canonicalizeHAURL(url?: string): string | undefined {
|
||||
protected _canonicalizeHAURL(url?: string): string | null {
|
||||
if (this.hass && url && url.startsWith('/')) {
|
||||
return this.hass.hassUrl(url);
|
||||
}
|
||||
return url;
|
||||
return url ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -551,44 +449,40 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
* @param index The index of the slide to lazy load.
|
||||
* @param slide The slide to lazy load.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected _lazyloadSlide(index: number, slide: HTMLElement): void {
|
||||
const childIndex: number | undefined = this._slideToChild[index];
|
||||
|
||||
if (
|
||||
childIndex === undefined ||
|
||||
!this.hass ||
|
||||
!this.view ||
|
||||
!this.view.target ||
|
||||
!this.view.target.children ||
|
||||
!isTrueMedia(this.view.target.children[childIndex])
|
||||
) {
|
||||
if (!this.hass || !this.view || !this.view.query || !this.cameras) {
|
||||
return;
|
||||
}
|
||||
|
||||
resolveMedia(
|
||||
this.hass,
|
||||
this.view.target.children[childIndex],
|
||||
this.resolvedMediaCache,
|
||||
).then((resolvedMedia) => {
|
||||
if (!resolvedMedia) {
|
||||
return;
|
||||
}
|
||||
const media = this.view.queryResults?.getResult(index);
|
||||
const mediaContentID = media
|
||||
? media.getContentID(this.cameras.get(media.getCameraID()))
|
||||
: null;
|
||||
if (!mediaContentID) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Snapshots.
|
||||
const img = slide.querySelector('img') as HTMLImageElement;
|
||||
resolveMedia(this.hass, mediaContentID, this.resolvedMediaCache).then(
|
||||
(resolvedMedia) => {
|
||||
if (!resolvedMedia) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Frigate >= 0.9.0+ clips.
|
||||
const hls_player = this._getPlayer(slide) as FrigateCardMediaPlayer & {
|
||||
url: string;
|
||||
};
|
||||
// Snapshots.
|
||||
const img = slide.querySelector('img') as HTMLImageElement;
|
||||
|
||||
if (img) {
|
||||
img.src = this._canonicalizeHAURL(resolvedMedia.url) || '';
|
||||
} else if (hls_player) {
|
||||
hls_player.url = this._canonicalizeHAURL(resolvedMedia.url) || '';
|
||||
}
|
||||
});
|
||||
// Frigate >= 0.9.0+ clips.
|
||||
const hls_player = this._getPlayer(slide) as FrigateCardMediaPlayer & {
|
||||
url: string;
|
||||
};
|
||||
|
||||
if (img) {
|
||||
img.src = this._canonicalizeHAURL(resolvedMedia.url) ?? '';
|
||||
} else if (hls_player) {
|
||||
hls_player.url = this._canonicalizeHAURL(resolvedMedia.url) ?? '';
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -596,21 +490,18 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
* @returns The slides to include in the render.
|
||||
*/
|
||||
protected _getSlides(): TemplateResult[] {
|
||||
if (
|
||||
!this.view ||
|
||||
!this.view.target ||
|
||||
!this.view.target.children ||
|
||||
!this.view.target.children.length
|
||||
) {
|
||||
if (!this.view || !this.view.queryResults) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const slides: TemplateResult[] = [];
|
||||
for (let i = 0; i < this.view.target.children?.length; ++i) {
|
||||
const slide = this._renderMediaItem(this.view.target.children[i], slides.length);
|
||||
|
||||
if (slide) {
|
||||
slides.push(slide);
|
||||
for (let i = 0; i < this.view.queryResults.getResultsCount(); ++i) {
|
||||
const media = this.view.queryResults.getResult(i);
|
||||
if (media) {
|
||||
const slide = this._renderMediaItem(media, i);
|
||||
if (slide) {
|
||||
slides[i] = slide;
|
||||
}
|
||||
}
|
||||
}
|
||||
return slides;
|
||||
@@ -620,8 +511,12 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
* 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)) {
|
||||
if (!this.resolvedMediaCache || !this.cameras) {
|
||||
return false;
|
||||
}
|
||||
for (const media of this.view?.queryResults?.getResults() ?? []) {
|
||||
const mediaContentID = media.getContentID(this.cameras.get(media.getCameraID()));
|
||||
if (mediaContentID && !this.resolvedMediaCache.has(mediaContentID)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -633,29 +528,20 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
* @param changedProps The changed properties
|
||||
*/
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
// Pre-populate a map between real media slides and view child indicies.
|
||||
if (changedProps.has('view')) {
|
||||
this._slideToChild = {};
|
||||
let i = 0;
|
||||
(this.view?.target?.children ?? []).forEach((child, index) => {
|
||||
if (isTrueMedia(child) && ['video', 'image'].includes(child.media_content_type)) {
|
||||
this._slideToChild[i++] = index;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (changedProps.has('viewerConfig')) {
|
||||
updateElementStyleFromMediaLayoutConfig(this, this.viewerConfig?.layout);
|
||||
}
|
||||
if (!this._carouselOptions || changedProps.has('viewerConfig')) {
|
||||
this._carouselOptions = this._getOptions();
|
||||
this._carouselOptions = {
|
||||
draggable: this.viewerConfig?.draggable ?? true,
|
||||
};
|
||||
}
|
||||
if (
|
||||
!this._carouselPlugins ||
|
||||
changedProps.has('viewerConfig') ||
|
||||
(changedProps.has('view') &&
|
||||
this.view?.target?.children?.length !==
|
||||
changedProps.get('view')?.target?.children?.length)
|
||||
this.view?.queryResults?.getResultsCount() !==
|
||||
changedProps.get('view')?.queryResults?.getResultsCount())
|
||||
) {
|
||||
this._carouselPlugins = this._getPlugins();
|
||||
}
|
||||
@@ -680,21 +566,20 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
* @returns A template to display to the user.
|
||||
*/
|
||||
protected _render(): TemplateResult | void {
|
||||
const slides = this._getSlides();
|
||||
|
||||
if (!slides.length || !this.view?.media) {
|
||||
const media = this.view?.queryResults?.getSelectedResult();
|
||||
if (!media || !this.cameras) {
|
||||
return;
|
||||
}
|
||||
|
||||
const neighbors = this._getMediaNeighbors();
|
||||
const [prev, next] = [neighbors?.previous, neighbors?.next];
|
||||
const [prev, next] = this._getMediaNeighbors();
|
||||
|
||||
return html` <frigate-card-media-carousel
|
||||
${ref(this._refMediaCarousel)}
|
||||
.carouselOptions=${this._carouselOptions}
|
||||
.carouselPlugins=${this._carouselPlugins}
|
||||
.label="${this.view.media.title}"
|
||||
.label=${media.getTitle() ?? undefined}
|
||||
.titlePopupConfig=${this.viewerConfig?.controls.title}
|
||||
.selected=${this.view?.queryResults?.getSelectedIndex() ?? 0}
|
||||
transitionEffect=${this._getTransitionEffect()}
|
||||
@frigate-card:media-carousel:select=${this._setViewHandler.bind(this)}
|
||||
@frigate-card:media:loaded=${this._recordingSeekHandler.bind(this)}
|
||||
@@ -704,22 +589,24 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.direction=${'previous'}
|
||||
.controlConfig=${this.viewerConfig?.controls.next_previous}
|
||||
.thumbnail=${prev && prev.thumbnail ? prev.thumbnail : undefined}
|
||||
.label=${prev ? prev.title : ''}
|
||||
.thumbnail=${prev?.getThumbnail(this.cameras.get(prev.getCameraID())) ??
|
||||
undefined}
|
||||
.label=${prev?.getTitle() ?? ''}
|
||||
?disabled=${!prev}
|
||||
@click=${(ev) => {
|
||||
this._refMediaCarousel.value?.frigateCardCarousel()?.carouselScrollPrevious();
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
></frigate-card-next-previous-control>
|
||||
${slides}
|
||||
${guard(this.view?.queryResults?.getResults(), () => this._getSlides())}
|
||||
<frigate-card-next-previous-control
|
||||
slot="next"
|
||||
.hass=${this.hass}
|
||||
.direction=${'next'}
|
||||
.controlConfig=${this.viewerConfig?.controls.next_previous}
|
||||
.thumbnail=${next && next.thumbnail ? next.thumbnail : undefined}
|
||||
.label=${next ? next.title : ''}
|
||||
.thumbnail=${next?.getThumbnail(this.cameras.get(next.getCameraID())) ??
|
||||
undefined}
|
||||
.label=${next?.getTitle() ?? ''}
|
||||
?disabled=${!next}
|
||||
@click=${(ev) => {
|
||||
this._refMediaCarousel.value?.frigateCardCarousel()?.carouselScrollNext();
|
||||
@@ -733,10 +620,12 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
* Fire a media show event when a slide is selected.
|
||||
*/
|
||||
protected _recordingSeekHandler(): void {
|
||||
const player = this._getPlayer();
|
||||
const childIndex = this.view?.childIndex ?? null;
|
||||
const selectedIndex = this.view?.queryResults?.getSelectedIndex() ?? null;
|
||||
const seek =
|
||||
childIndex !== null ? this.view?.context?.mediaViewer?.seek.get(childIndex) : null;
|
||||
selectedIndex !== null
|
||||
? this.view?.context?.mediaViewer?.seek.get(selectedIndex)
|
||||
: null;
|
||||
const player = this._getPlayer();
|
||||
if (player && seek) {
|
||||
player.seek(seek.seekSeconds);
|
||||
}
|
||||
@@ -744,59 +633,53 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
|
||||
/**
|
||||
* Render a single media item in the viewer carousel.
|
||||
* @param mediaToRender The FrigateBrowseMediaSource to render.
|
||||
* @param slideIndex The index of the slide to render.
|
||||
* @param media The ViewMedia to render.
|
||||
* @param index The (slide|queryResult) index of the item to render.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected _renderMediaItem(
|
||||
mediaToRender: FrigateBrowseMediaSource,
|
||||
slideIndex: number,
|
||||
): TemplateResult | void {
|
||||
protected _renderMediaItem(media: ViewMedia, index: number): TemplateResult | null {
|
||||
// Skip folders as they cannot be rendered by this viewer.
|
||||
if (
|
||||
!this.hass ||
|
||||
!this.view ||
|
||||
!this.viewerConfig ||
|
||||
!isTrueMedia(mediaToRender) ||
|
||||
!['video', 'image'].includes(mediaToRender.media_content_type)
|
||||
) {
|
||||
return;
|
||||
if (!this.hass || !this.view || !this.viewerConfig || !this.cameras) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lazyLoad = this.viewerConfig.lazy_load;
|
||||
const resolvedMedia = this.resolvedMediaCache?.get(mediaToRender.media_content_id);
|
||||
const mediaContentID = media.getContentID(this.cameras.get(media.getCameraID()));
|
||||
const resolvedMedia = mediaContentID
|
||||
? this.resolvedMediaCache?.get(mediaContentID)
|
||||
: null;
|
||||
if (!resolvedMedia && !lazyLoad) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// The media is attached to the player as '.media' which is used in
|
||||
// `_selectSlideMediaShowHandler` (and not used by the player itself).
|
||||
return html`
|
||||
<div class="embla__slide">
|
||||
${mediaToRender.media_content_type === 'video'
|
||||
${media.isVideo()
|
||||
? html`<frigate-card-ha-hls-player
|
||||
allow-exoplayer
|
||||
aria-label="${mediaToRender.title}"
|
||||
aria-label="${media.getTitle() ?? ''}"
|
||||
?autoplay=${false}
|
||||
controls
|
||||
muted
|
||||
playsinline
|
||||
title="${mediaToRender.title}"
|
||||
title="${media.getTitle() ?? ''}"
|
||||
url=${ifDefined(
|
||||
lazyLoad ? undefined : this._canonicalizeHAURL(resolvedMedia?.url),
|
||||
lazyLoad ? undefined : this._canonicalizeHAURL(resolvedMedia?.url) ?? '',
|
||||
)}
|
||||
.hass=${this.hass}
|
||||
@frigate-card:media:loaded=${(e: CustomEvent<MediaLoadedInfo>) => {
|
||||
wrapMediaLoadedEventForCarousel(slideIndex, e);
|
||||
wrapMediaLoadedEventForCarousel(index, e);
|
||||
}}
|
||||
>
|
||||
</frigate-card-ha-hls-player>`
|
||||
: html`<img
|
||||
aria-label="${mediaToRender.title}"
|
||||
aria-label="${media.getTitle() ?? ''}"
|
||||
src=${ifDefined(
|
||||
lazyLoad ? IMG_EMPTY : this._canonicalizeHAURL(resolvedMedia?.url),
|
||||
lazyLoad ? IMG_EMPTY : this._canonicalizeHAURL(resolvedMedia?.url) ?? '',
|
||||
)}
|
||||
title="${mediaToRender.title}"
|
||||
title="${media.getTitle() ?? ''}"
|
||||
@click=${() => {
|
||||
if (
|
||||
this._refMediaCarousel.value
|
||||
@@ -804,7 +687,7 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
?.carouselClickAllowed() &&
|
||||
this.viewerConfig?.snapshot_click_plays_clip
|
||||
) {
|
||||
this._findRelatedClipView(mediaToRender).then((view) => {
|
||||
this._createRelatedClipView(index).then((view) => {
|
||||
if (view) {
|
||||
view.dispatchChangeEvent(this);
|
||||
}
|
||||
@@ -822,9 +705,9 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
// images in media-carousel.ts). Here we need to only call the
|
||||
// media load handler on a 'real' load.
|
||||
!lazyLoad ||
|
||||
lazyloadPlugin?.hasLazyloaded(slideIndex)
|
||||
lazyloadPlugin?.hasLazyloaded(index)
|
||||
) {
|
||||
wrapRawMediaLoadedEventForCarousel(slideIndex, e);
|
||||
wrapRawMediaLoadedEventForCarousel(index, e);
|
||||
}
|
||||
}}"
|
||||
/>`}
|
||||
|
||||
Reference in New Issue
Block a user