Skeleton live carousel.
This commit is contained in:
+51
-11
@@ -1,11 +1,17 @@
|
|||||||
import type { BrowseMediaQueryParameters, BrowseMediaSource, ExtendedHomeAssistant } from './types.js';
|
import type {
|
||||||
|
BrowseMediaQueryParameters,
|
||||||
|
BrowseMediaSource,
|
||||||
|
CameraConfig,
|
||||||
|
ExtendedHomeAssistant,
|
||||||
|
} from './types.js';
|
||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { homeAssistantWSRequest } from './common.js';
|
import { homeAssistantWSRequest } from './common.js';
|
||||||
import { browseMediaSourceSchema } from './types.js';
|
import { browseMediaSourceSchema } from './types.js';
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import dayjs_custom_parse_format from 'dayjs/plugin/customParseFormat.js';
|
import dayjs_custom_parse_format from 'dayjs/plugin/customParseFormat.js';
|
||||||
|
import { View } from './view.js';
|
||||||
|
|
||||||
dayjs.extend(dayjs_custom_parse_format);
|
dayjs.extend(dayjs_custom_parse_format);
|
||||||
|
|
||||||
export class BrowseMediaUtil {
|
export class BrowseMediaUtil {
|
||||||
@@ -16,7 +22,8 @@ export class BrowseMediaUtil {
|
|||||||
*/
|
*/
|
||||||
static extractEventID(media: BrowseMediaSource): string | null {
|
static extractEventID(media: BrowseMediaSource): string | null {
|
||||||
const result = media.media_content_id.match(
|
const result = media.media_content_id.match(
|
||||||
/^media-source:\/\/frigate\/.*\/(?<id>[.0-9]+-[a-zA-Z0-9]+)$/);
|
/^media-source:\/\/frigate\/.*\/(?<id>[.0-9]+-[a-zA-Z0-9]+)$/,
|
||||||
|
);
|
||||||
return result && result.groups ? result.groups['id'] : null;
|
return result && result.groups ? result.groups['id'] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,9 +32,7 @@ export class BrowseMediaUtil {
|
|||||||
* @param browseMedia The media object to extract the start time from.
|
* @param browseMedia The media object to extract the start time from.
|
||||||
* @returns The start time in unix/epoch time, or null if it cannot be determined.
|
* @returns The start time in unix/epoch time, or null if it cannot be determined.
|
||||||
*/
|
*/
|
||||||
static extractEventStartTime(
|
static extractEventStartTime(browseMedia: BrowseMediaSource): number | null {
|
||||||
browseMedia: BrowseMediaSource,
|
|
||||||
): number | null {
|
|
||||||
// Example: 2021-08-27 20:57:22 [10s, Person 76%]
|
// Example: 2021-08-27 20:57:22 [10s, Person 76%]
|
||||||
const result = browseMedia.title.match(/^(?<iso_datetime>.+) \[/);
|
const result = browseMedia.title.match(/^(?<iso_datetime>.+) \[/);
|
||||||
if (result && result.groups) {
|
if (result && result.groups) {
|
||||||
@@ -57,16 +62,14 @@ export class BrowseMediaUtil {
|
|||||||
* @param media The media object with children.
|
* @param media The media object with children.
|
||||||
* @returns The first true media item found.
|
* @returns The first true media item found.
|
||||||
*/
|
*/
|
||||||
static getFirstTrueMediaChildIndex(
|
static getFirstTrueMediaChildIndex(media: BrowseMediaSource | null): number | null {
|
||||||
media: BrowseMediaSource | null,
|
|
||||||
): number | null {
|
|
||||||
if (!media || !media.children) {
|
if (!media || !media.children) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const index = media.children.findIndex((child) => this.isTrueMedia(child));
|
const index = media.children.findIndex((child) => this.isTrueMedia(child));
|
||||||
return index >= 0 ? index : null;
|
return index >= 0 ? index : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browse Frigate media with a media content id. May throw.
|
* Browse Frigate media with a media content id. May throw.
|
||||||
* @param hass The HomeAssistant object.
|
* @param hass The HomeAssistant object.
|
||||||
@@ -86,7 +89,7 @@ export class BrowseMediaUtil {
|
|||||||
};
|
};
|
||||||
return homeAssistantWSRequest(hass, browseMediaSourceSchema, request);
|
return homeAssistantWSRequest(hass, browseMediaSourceSchema, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Browse Frigate media with query parameters.
|
// Browse Frigate media with query parameters.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,4 +120,41 @@ export class BrowseMediaUtil {
|
|||||||
].join('/'),
|
].join('/'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the parameters to search for media.
|
||||||
|
* @returns A BrowseMediaQueryParameters object.
|
||||||
|
*/
|
||||||
|
static getBrowseMediaQueryParameters(
|
||||||
|
mediaType: 'clips' | 'snapshots',
|
||||||
|
cameraConfig?: CameraConfig,
|
||||||
|
): BrowseMediaQueryParameters | undefined {
|
||||||
|
if (!cameraConfig || !cameraConfig.camera_name) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mediaType: mediaType,
|
||||||
|
clientId: cameraConfig.client_id,
|
||||||
|
cameraName: cameraConfig.camera_name,
|
||||||
|
label: cameraConfig.label,
|
||||||
|
zone: cameraConfig.zone,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the parameters to search for media related to the current view.
|
||||||
|
* @returns A BrowseMediaQueryParameters object.
|
||||||
|
*/
|
||||||
|
static getBrowseMediaQueryParametersFromView(
|
||||||
|
view: View,
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
): BrowseMediaQueryParameters | undefined {
|
||||||
|
if (!view.isClipRelatedView() && !view.isSnapshotRelatedView()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return BrowseMediaUtil.getBrowseMediaQueryParameters(
|
||||||
|
view.isClipRelatedView() ? 'clips' : 'snapshots',
|
||||||
|
cameraConfig,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-36
@@ -884,36 +884,6 @@ export class FrigateCard extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the parameters to search for media related to the current view.
|
|
||||||
* @returns A BrowseMediaQueryParameters object.
|
|
||||||
*/
|
|
||||||
protected _getBrowseMediaQueryParameters(
|
|
||||||
mediaType?: 'clips' | 'snapshots',
|
|
||||||
): BrowseMediaQueryParameters | undefined {
|
|
||||||
const cameraConfig = this._getSelectedCameraConfig();
|
|
||||||
|
|
||||||
if (
|
|
||||||
!cameraConfig ||
|
|
||||||
!cameraConfig.camera_name ||
|
|
||||||
!this._view ||
|
|
||||||
!(
|
|
||||||
this._view.isClipRelatedView() ||
|
|
||||||
this._view.isSnapshotRelatedView() ||
|
|
||||||
mediaType
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
mediaType: mediaType || (this._view.isClipRelatedView() ? 'clips' : 'snapshots'),
|
|
||||||
clientId: cameraConfig.client_id,
|
|
||||||
cameraName: cameraConfig.camera_name,
|
|
||||||
label: cameraConfig.label,
|
|
||||||
zone: cameraConfig.zone,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for media play event.
|
* Handler for media play event.
|
||||||
*/
|
*/
|
||||||
@@ -1158,7 +1128,10 @@ export class FrigateCard extends LitElement {
|
|||||||
? html` <frigate-card-gallery
|
? html` <frigate-card-gallery
|
||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.view=${this._view}
|
.view=${this._view}
|
||||||
.browseMediaQueryParameters=${this._getBrowseMediaQueryParameters()}
|
.browseMediaQueryParameters=${BrowseMediaUtil.getBrowseMediaQueryParametersFromView(
|
||||||
|
this._view,
|
||||||
|
cameraConfig,
|
||||||
|
)}
|
||||||
class="${classMap(galleryClasses)}"
|
class="${classMap(galleryClasses)}"
|
||||||
@frigate-card:change-view=${this._changeViewHandler}
|
@frigate-card:change-view=${this._changeViewHandler}
|
||||||
@frigate-card:message=${this._messageHandler}
|
@frigate-card:message=${this._messageHandler}
|
||||||
@@ -1169,7 +1142,10 @@ export class FrigateCard extends LitElement {
|
|||||||
? html` <frigate-card-viewer
|
? html` <frigate-card-viewer
|
||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.view=${this._view}
|
.view=${this._view}
|
||||||
.browseMediaQueryParameters=${this._getBrowseMediaQueryParameters()}
|
.browseMediaQueryParameters=${BrowseMediaUtil.getBrowseMediaQueryParametersFromView(
|
||||||
|
this._view,
|
||||||
|
cameraConfig,
|
||||||
|
)}
|
||||||
.viewerConfig=${this.config.event_viewer}
|
.viewerConfig=${this.config.event_viewer}
|
||||||
.resolvedMediaCache=${this._resolvedMediaCache}
|
.resolvedMediaCache=${this._resolvedMediaCache}
|
||||||
class="${classMap(viewerClasses)}"
|
class="${classMap(viewerClasses)}"
|
||||||
@@ -1189,11 +1165,8 @@ export class FrigateCard extends LitElement {
|
|||||||
<frigate-card-live
|
<frigate-card-live
|
||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.view=${this._view}
|
.view=${this._view}
|
||||||
.browseMediaQueryParameters=${this._getBrowseMediaQueryParameters(
|
|
||||||
this.config.live.controls.thumbnails.media,
|
|
||||||
)}
|
|
||||||
.liveConfig=${this.config.live}
|
.liveConfig=${this.config.live}
|
||||||
.cameraConfig=${cameraConfig}
|
.cameras=${this._cameras}
|
||||||
.preload=${this.config.live.preload && !this._view.is('live')}
|
.preload=${this.config.live.preload && !this._view.is('live')}
|
||||||
class="${classMap(liveClasses)}"
|
class="${classMap(liveClasses)}"
|
||||||
@frigate-card:change-view=${this._changeViewHandler}
|
@frigate-card:change-view=${this._changeViewHandler}
|
||||||
|
|||||||
+17
-14
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { CSSResultGroup, LitElement, unsafeCSS, PropertyValues } from 'lit';
|
||||||
CSSResultGroup,
|
|
||||||
LitElement,
|
|
||||||
unsafeCSS,
|
|
||||||
PropertyValues,
|
|
||||||
} from 'lit';
|
|
||||||
import EmblaCarousel, { EmblaCarouselType, EmblaOptionsType } from 'embla-carousel';
|
import EmblaCarousel, { EmblaCarouselType, EmblaOptionsType } from 'embla-carousel';
|
||||||
|
|
||||||
import { dispatchFrigateCardEvent } from '../common';
|
import { dispatchFrigateCardEvent } from '../common';
|
||||||
@@ -15,7 +10,6 @@ export interface CarouselSelect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class FrigateCardCarousel extends LitElement {
|
export class FrigateCardCarousel extends LitElement {
|
||||||
protected _options?: EmblaOptionsType;
|
|
||||||
protected _carousel?: EmblaCarouselType;
|
protected _carousel?: EmblaCarouselType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,11 +35,17 @@ export class FrigateCardCarousel extends LitElement {
|
|||||||
updated(changedProperties: PropertyValues): void {
|
updated(changedProperties: PropertyValues): void {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
if (!this._carousel) {
|
this.updateComplete.then(() => {
|
||||||
this.updateComplete.then(() => {
|
this._loadCarousel();
|
||||||
this._loadCarousel();
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Get the Embla options to use.
|
||||||
|
* @returns An EmblaOptionsType object or undefined for no options.
|
||||||
|
*/
|
||||||
|
protected _getOptions(): EmblaOptionsType | undefined {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,8 +56,11 @@ export class FrigateCardCarousel extends LitElement {
|
|||||||
'.embla__viewport',
|
'.embla__viewport',
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
|
|
||||||
if (!this._carousel && carouselNode) {
|
if (carouselNode) {
|
||||||
this._carousel = EmblaCarousel(carouselNode, this._options);
|
if (this._carousel) {
|
||||||
|
this._carousel.destroy();
|
||||||
|
}
|
||||||
|
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', () => {
|
||||||
const selected = this.carouselSelected();
|
const selected = this.carouselSelected();
|
||||||
|
|||||||
+212
-35
@@ -1,6 +1,10 @@
|
|||||||
|
// TODO controls
|
||||||
|
// TODO media events
|
||||||
|
// TODO lazy loading
|
||||||
|
// TODO height adapting
|
||||||
|
|
||||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||||
import type {
|
import type {
|
||||||
BrowseMediaQueryParameters,
|
|
||||||
BrowseMediaSource,
|
BrowseMediaSource,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
@@ -9,11 +13,13 @@ import type {
|
|||||||
MediaShowInfo,
|
MediaShowInfo,
|
||||||
WebRTCConfig,
|
WebRTCConfig,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
|
import { EmblaCarouselType, EmblaOptionsType } from 'embla-carousel';
|
||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { until } from 'lit/directives/until.js';
|
import { until } from 'lit/directives/until.js';
|
||||||
|
|
||||||
import { BrowseMediaUtil } from '../browse-media-util.js';
|
import { BrowseMediaUtil } from '../browse-media-util.js';
|
||||||
|
import { FrigateCardCarousel } from './carousel.js';
|
||||||
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
||||||
import { View } from '../view.js';
|
import { View } from '../view.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
@@ -34,6 +40,7 @@ import liveStyle from '../scss/live.scss';
|
|||||||
import liveFrigateStyle from '../scss/live-frigate.scss';
|
import liveFrigateStyle from '../scss/live-frigate.scss';
|
||||||
import liveJSMPEGStyle from '../scss/live-jsmpeg.scss';
|
import liveJSMPEGStyle from '../scss/live-jsmpeg.scss';
|
||||||
import liveWebRTCStyle from '../scss/live-webrtc.scss';
|
import liveWebRTCStyle from '../scss/live-webrtc.scss';
|
||||||
|
import mediaCarouselStyle from '../scss/media-carousel.scss';
|
||||||
|
|
||||||
// Number of seconds a signed URL is valid for.
|
// Number of seconds a signed URL is valid for.
|
||||||
const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
||||||
@@ -50,14 +57,11 @@ export class FrigateCardLive extends LitElement {
|
|||||||
protected view?: View;
|
protected view?: View;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected cameraConfig?: CameraConfig;
|
protected cameras?: Map<string, CameraConfig>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected liveConfig?: LiveConfig;
|
protected liveConfig?: LiveConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
protected browseMediaQueryParameters?: BrowseMediaQueryParameters;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
set preload(preload: boolean) {
|
set preload(preload: boolean) {
|
||||||
this._preload = preload;
|
this._preload = preload;
|
||||||
@@ -97,15 +101,19 @@ export class FrigateCardLive extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetchThumbnailsThenRender = async (): Promise<TemplateResult | void> => {
|
const fetchThumbnailsThenRender = async (): Promise<TemplateResult | void> => {
|
||||||
if (!this.hass || !this.browseMediaQueryParameters) {
|
if (!this.hass || !this.cameras || !this.view || !this.liveConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const browseMediaParams = BrowseMediaUtil.getBrowseMediaQueryParameters(
|
||||||
|
this.liveConfig.controls.thumbnails.media,
|
||||||
|
this.cameras.get(this.view.camera),
|
||||||
|
);
|
||||||
|
if (!browseMediaParams) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let parent: BrowseMediaSource | null;
|
let parent: BrowseMediaSource | null;
|
||||||
try {
|
try {
|
||||||
parent = await BrowseMediaUtil.browseMediaQuery(
|
parent = await BrowseMediaUtil.browseMediaQuery(this.hass, browseMediaParams);
|
||||||
this.hass,
|
|
||||||
this.browseMediaQueryParameters,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return dispatchErrorMessageEvent(this, (e as Error).message);
|
return dispatchErrorMessageEvent(this, (e as Error).message);
|
||||||
}
|
}
|
||||||
@@ -113,10 +121,11 @@ export class FrigateCardLive extends LitElement {
|
|||||||
if (BrowseMediaUtil.getFirstTrueMediaChildIndex(parent) != null) {
|
if (BrowseMediaUtil.getFirstTrueMediaChildIndex(parent) != null) {
|
||||||
return html` <frigate-card-thumbnail-carousel
|
return html` <frigate-card-thumbnail-carousel
|
||||||
.target=${parent}
|
.target=${parent}
|
||||||
|
.view=${this.view}
|
||||||
.config=${this.liveConfig?.controls.thumbnails}
|
.config=${this.liveConfig?.controls.thumbnails}
|
||||||
.highlightSelected=${false}
|
.highlightSelected=${false}
|
||||||
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
|
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
|
||||||
const mediaType = this.browseMediaQueryParameters?.mediaType;
|
const mediaType = browseMediaParams.mediaType;
|
||||||
if (mediaType && this.view && ['snapshots', 'clips'].includes(mediaType)) {
|
if (mediaType && this.view && ['snapshots', 'clips'].includes(mediaType)) {
|
||||||
new View({
|
new View({
|
||||||
view: mediaType === 'clips' ? 'clip-specific' : 'snapshot-specific',
|
view: mediaType === 'clips' ? 'clip-specific' : 'snapshot-specific',
|
||||||
@@ -139,7 +148,7 @@ export class FrigateCardLive extends LitElement {
|
|||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.hass || !this.liveConfig || !this.cameraConfig) {
|
if (!this.hass || !this.liveConfig || !this.cameras) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,28 +156,19 @@ export class FrigateCardLive extends LitElement {
|
|||||||
${this.liveConfig.controls.thumbnails.mode === 'above'
|
${this.liveConfig.controls.thumbnails.mode === 'above'
|
||||||
? this.renderThumbnails()
|
? this.renderThumbnails()
|
||||||
: ''}
|
: ''}
|
||||||
${this.liveConfig.provider == 'frigate'
|
<frigate-card-live-carousel
|
||||||
? html` <frigate-card-live-frigate
|
.hass=${this.hass}
|
||||||
.hass=${this.hass}
|
.view=${this.view}
|
||||||
.cameraEntity=${this.cameraConfig.camera_entity}
|
.cameras=${this.cameras}
|
||||||
@frigate-card:media-show=${this._mediaShowHandler}
|
.liveConfig=${this.liveConfig}
|
||||||
>
|
@frigate-card:media-show=${this._mediaShowHandler}
|
||||||
</frigate-card-live-frigate>`
|
@frigate-card:carousel:select=${() => {
|
||||||
: this.liveConfig.provider == 'webrtc'
|
// Re-rendering the component will cause the thumbnails to be
|
||||||
? html`<frigate-card-live-webrtc
|
// re-fetched.
|
||||||
.hass=${this.hass}
|
this.requestUpdate();
|
||||||
.webRTCConfig=${this.liveConfig.webrtc || {}}
|
}}
|
||||||
@frigate-card:media-show=${this._mediaShowHandler}
|
>
|
||||||
>
|
</frigate-card-live-carousel>
|
||||||
</frigate-card-live-webrtc>`
|
|
||||||
: html` <frigate-card-live-jsmpeg
|
|
||||||
.hass=${this.hass}
|
|
||||||
.cameraName=${this.cameraConfig.camera_name}
|
|
||||||
.clientId=${this.cameraConfig.client_id}
|
|
||||||
.jsmpegConfig=${this.liveConfig.jsmpeg}
|
|
||||||
@frigate-card:media-show=${this._mediaShowHandler}
|
|
||||||
>
|
|
||||||
</frigate-card-live-jsmpeg>`}
|
|
||||||
${this.liveConfig.controls.thumbnails.mode === 'below'
|
${this.liveConfig.controls.thumbnails.mode === 'below'
|
||||||
? this.renderThumbnails()
|
? this.renderThumbnails()
|
||||||
: ''}
|
: ''}
|
||||||
@@ -183,6 +183,183 @@ export class FrigateCardLive extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@customElement('frigate-card-live-carousel')
|
||||||
|
export class FrigateCardLiveCarousel extends FrigateCardCarousel {
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected view?: View;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected cameras?: Map<string, CameraConfig>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected liveConfig?: LiveConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Embla options to use.
|
||||||
|
* @returns An EmblaOptionsType object or undefined for no options.
|
||||||
|
*/
|
||||||
|
protected _getOptions(): EmblaOptionsType {
|
||||||
|
let startIndex = -1;
|
||||||
|
if (this.cameras && this.view) {
|
||||||
|
startIndex = Array.from(this.cameras.keys()).indexOf(this.view.camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
startIndex: startIndex < 0 ? undefined : startIndex,
|
||||||
|
// TODO: draggable: this.viewerConfig.draggable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the carousel.
|
||||||
|
*/
|
||||||
|
protected _loadCarousel(): void {
|
||||||
|
super._loadCarousel();
|
||||||
|
|
||||||
|
// Necessary because typescript local type narrowing is not paying attention
|
||||||
|
// to the side-effect of the call to super._loadCarousel().
|
||||||
|
const carousel = this._carousel as EmblaCarouselType | undefined;
|
||||||
|
carousel?.on('select', this._selectSlideSetViewHandler.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get slides to include in the render.
|
||||||
|
* @returns The slides to include in the render.
|
||||||
|
*/
|
||||||
|
protected _getSlides(): TemplateResult[] {
|
||||||
|
if (!this.cameras) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return Array.from(this.cameras.values()).map(this._renderLive.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Handle the user selecting a new slide in the carousel.
|
||||||
|
// */
|
||||||
|
protected _selectSlideSetViewHandler(): void {
|
||||||
|
if (!this._carousel || !this.view || !this.cameras) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedSnap = this._carousel.selectedScrollSnap();
|
||||||
|
this.view.camera = Array.from(this.cameras.keys())[selectedSnap];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _renderLive(cameraConfig: CameraConfig): TemplateResult {
|
||||||
|
// TODO: Add lazy load
|
||||||
|
return html` <div class="embla__slide">
|
||||||
|
<frigate-card-live-provider
|
||||||
|
.hass=${this.hass}
|
||||||
|
.cameraConfig=${cameraConfig}
|
||||||
|
.liveConfig=${this.liveConfig}
|
||||||
|
>
|
||||||
|
</frigate-card-live-provider>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the element.
|
||||||
|
* @returns A template to display to the user.
|
||||||
|
*/
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
const slides = this._getSlides();
|
||||||
|
if (!slides) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ${neighbors && neighbors.previous
|
||||||
|
// ? html`<frigate-card-next-previous-control
|
||||||
|
// .direction=${'previous'}
|
||||||
|
// .controlConfig=${this.viewerConfig?.controls.next_previous}
|
||||||
|
// .thumbnail=${neighbors.previous.thumbnail}
|
||||||
|
// .title=${neighbors.previous.title}
|
||||||
|
// .actionHandler=${actionHandler({
|
||||||
|
// hasHold: false,
|
||||||
|
// hasDoubleClick: false,
|
||||||
|
// })}
|
||||||
|
// @action=${() => {
|
||||||
|
// this._nextPreviousHandler('previous');
|
||||||
|
// }}
|
||||||
|
// ></frigate-card-next-previous-control>`
|
||||||
|
// : ``}
|
||||||
|
|
||||||
|
return html`<div class="embla">
|
||||||
|
<div class="embla__viewport">
|
||||||
|
<div class="embla__container">${slides}</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
// ${neighbors && neighbors.next
|
||||||
|
// ? html`<frigate-card-next-previous-control
|
||||||
|
// .direction=${'next'}
|
||||||
|
// .controlConfig=${this.viewerConfig?.controls.next_previous}
|
||||||
|
// .thumbnail=${neighbors.next.thumbnail}
|
||||||
|
// .title=${neighbors.next.title}
|
||||||
|
// .actionHandler=${actionHandler({
|
||||||
|
// hasHold: false,
|
||||||
|
// hasDoubleClick: false,
|
||||||
|
// })}
|
||||||
|
// @action=${() => {
|
||||||
|
// this._nextPreviousHandler('next');
|
||||||
|
// }}
|
||||||
|
// ></frigate-card-next-previous-control>`
|
||||||
|
// : ``}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get element styles.
|
||||||
|
*/
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [super.styles, unsafeCSS(mediaCarouselStyle)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('frigate-card-live-provider')
|
||||||
|
export class FrigateCardLiveProvider extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected hass?: HomeAssistant & ExtendedHomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected cameraConfig?: CameraConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected liveConfig?: LiveConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Master render method.
|
||||||
|
* @returns A rendered template.
|
||||||
|
*/
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this.hass || !this.liveConfig || !this.cameraConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${this.liveConfig.provider == 'frigate'
|
||||||
|
? html` <frigate-card-live-frigate
|
||||||
|
.hass=${this.hass}
|
||||||
|
.cameraEntity=${this.cameraConfig.camera_entity}
|
||||||
|
>
|
||||||
|
</frigate-card-live-frigate>`
|
||||||
|
: this.liveConfig.provider == 'webrtc'
|
||||||
|
? html`<frigate-card-live-webrtc
|
||||||
|
.hass=${this.hass}
|
||||||
|
.webRTCConfig=${this.liveConfig.webrtc || {}}
|
||||||
|
>
|
||||||
|
</frigate-card-live-webrtc>`
|
||||||
|
: html` <frigate-card-live-jsmpeg
|
||||||
|
.hass=${this.hass}
|
||||||
|
.cameraName=${this.cameraConfig.camera_name}
|
||||||
|
.clientId=${this.cameraConfig.client_id}
|
||||||
|
.jsmpegConfig=${this.liveConfig.jsmpeg}
|
||||||
|
>
|
||||||
|
</frigate-card-live-jsmpeg>`}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@customElement('frigate-card-live-frigate')
|
@customElement('frigate-card-live-frigate')
|
||||||
export class FrigateCardLiveFrigate extends LitElement {
|
export class FrigateCardLiveFrigate extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -478,7 +655,7 @@ export class FrigateCardLiveJSMPEG extends LitElement {
|
|||||||
return dispatchErrorMessageEvent(this, localize('error.jsmpeg_no_player'));
|
return dispatchErrorMessageEvent(this, localize('error.jsmpeg_no_player'));
|
||||||
}
|
}
|
||||||
return html`${this._jsmpegCanvasElement}`;
|
return html`${this._jsmpegCanvasElement}`;
|
||||||
}
|
};
|
||||||
return html`${until(_render(), renderProgressIndicator())}`;
|
return html`${until(_render(), renderProgressIndicator())}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BrowseMediaUtil } from '../browse-media-util.js';
|
import { BrowseMediaUtil } from '../browse-media-util.js';
|
||||||
import { CSSResultGroup, TemplateResult, html, unsafeCSS } from 'lit';
|
import { CSSResultGroup, TemplateResult, html, unsafeCSS } from 'lit';
|
||||||
|
import { EmblaOptionsType } from 'embla-carousel';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
|
||||||
import type { BrowseMediaSource, ThumbnailsControlConfig } from '../types.js';
|
import type { BrowseMediaSource, ThumbnailsControlConfig } from '../types.js';
|
||||||
@@ -38,9 +39,12 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
|||||||
this.style.setProperty('--frigate-card-carousel-thumbnail-opacity', value ? '0.6' : '1.0');
|
this.style.setProperty('--frigate-card-carousel-thumbnail-opacity', value ? '0.6' : '1.0');
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
/**
|
||||||
super();
|
* Get the Embla options to use.
|
||||||
this._options = {
|
* @returns An EmblaOptionsType object or undefined for no options.
|
||||||
|
*/
|
||||||
|
protected _getOptions(): EmblaOptionsType {
|
||||||
|
return {
|
||||||
containScroll: 'keepSnaps',
|
containScroll: 'keepSnaps',
|
||||||
dragFree: true,
|
dragFree: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||||
import { BrowseMediaUtil } from '../browse-media-util.js';
|
import { BrowseMediaUtil } from '../browse-media-util.js';
|
||||||
import { EmblaCarouselType } from 'embla-carousel';
|
import { EmblaCarouselType, EmblaOptionsType } from 'embla-carousel';
|
||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
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';
|
||||||
@@ -250,13 +250,10 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
|||||||
protected _slideHasBeenLazyLoaded: Record<number, boolean> = {};
|
protected _slideHasBeenLazyLoaded: Record<number, boolean> = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the carousel with "slides" (clips or snapshots).
|
* Get the Embla options to use.
|
||||||
|
* @returns An EmblaOptionsType object or undefined for no options.
|
||||||
*/
|
*/
|
||||||
protected _loadCarousel(): void {
|
protected _getOptions(): EmblaOptionsType {
|
||||||
if (this._carousel || !this.viewerConfig) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the carousel on the selected child number.
|
// Start the carousel on the selected child number.
|
||||||
const startIndex = Number(
|
const startIndex = Number(
|
||||||
Object.keys(this._slideToChild).find(
|
Object.keys(this._slideToChild).find(
|
||||||
@@ -264,11 +261,16 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this._options = {
|
return {
|
||||||
startIndex: isNaN(startIndex) ? undefined : startIndex,
|
startIndex: isNaN(startIndex) ? undefined : startIndex,
|
||||||
draggable: this.viewerConfig.draggable,
|
draggable: this.viewerConfig?.draggable,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the carousel with "slides" (clips or snapshots).
|
||||||
|
*/
|
||||||
|
protected _loadCarousel(): void {
|
||||||
super._loadCarousel();
|
super._loadCarousel();
|
||||||
|
|
||||||
// Necessary because typescript local type narrowing is not paying attention
|
// Necessary because typescript local type narrowing is not paying attention
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
frigate-card-live {
|
frigate-card-live-carousel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user