Refactor carousel into separate components.

This commit is contained in:
Dermot Duffy
2022-07-09 22:43:43 -07:00
parent 30fec98e18
commit dedecc24c6
18 changed files with 698 additions and 517 deletions
+93 -120
View File
@@ -15,12 +15,16 @@ import { customElement, property, state } from 'lit/decorators.js';
import { createRef, Ref, ref } from 'lit/directives/ref.js';
import { until } from 'lit/directives/until.js';
import { ConditionState, getOverriddenConfig } from '../card-condition.js';
import { dispatchFrigateCardErrorEvent, renderProgressIndicator } from '../components/message.js';
import {
dispatchFrigateCardErrorEvent,
renderProgressIndicator,
} from '../components/message.js';
import { localize } from '../localize/localize.js';
import liveFrigateStyle from '../scss/live-frigate.scss';
import liveJSMPEGStyle from '../scss/live-jsmpeg.scss';
import liveWebRTCStyle from '../scss/live-webrtc.scss';
import liveStyle from '../scss/live.scss';
import liveCarouselStyle from '../scss/live-carousel.scss';
import {
CameraConfig,
ExtendedHomeAssistant,
@@ -47,10 +51,9 @@ import {
import { View } from '../view.js';
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
import { Lazyload } from './embla-plugins/lazyload.js';
import { FrigateCardMediaCarousel } from './media-carousel.js';
import { FrigateCardMediaCarousel, wrapMediaShowEventForCarousel } from './media-carousel.js';
import { dispatchErrorMessageEvent } from './message.js';
import './next-prev-control.js';
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
import './title-control.js';
import './surround-thumbnails';
import '../patches/ha-camera-stream';
@@ -182,7 +185,7 @@ export class FrigateCardLive extends LitElement {
}
@customElement('frigate-card-live-carousel')
export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
export class FrigateCardLiveCarousel extends LitElement {
@property({ attribute: false })
public hass?: ExtendedHomeAssistant;
@@ -206,40 +209,39 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
// Index between camera name and slide number.
protected _cameraToSlide: Record<string, number> = {};
protected _refMediaCarousel: Ref<FrigateCardMediaCarousel> = createRef();
/**
* The updated lifecycle callback for this element.
* @param changedProperties The properties that were changed in this render.
*/
updated(changedProperties: PropertyValues): void {
if (
this._carousel &&
(changedProperties.has('cameras') || changedProperties.has('liveConfig'))
) {
// All of these properties may fundamentally change the contents/size of
// the DOM, and the carousel should be reset when they change.
this._destroyCarousel();
}
super.updated(changedProperties);
const frigateCardMediaCarousel = this._refMediaCarousel.value;
const frigateCardCarousel = frigateCardMediaCarousel?.frigateCardCarousel();
if (changedProperties.has('view')) {
const oldView = changedProperties.get('view') as View | undefined;
if (
this._carousel &&
frigateCardCarousel &&
oldView &&
this.view?.camera &&
this.view?.camera != oldView.camera
) {
const slide: number | undefined = this._cameraToSlide[this.view.camera];
if (slide !== undefined && slide !== this.carouselSelected()) {
this.carouselScrollTo(slide);
if (slide !== undefined && slide !== frigateCardCarousel.carouselSelected()) {
frigateCardCarousel.carouselScrollTo(slide);
}
}
}
if (changedProperties.has('preloaded')) {
const automedia = this._getAutoMediaPlugin();
if (
frigateCardMediaCarousel &&
frigateCardCarousel &&
changedProperties.has('preloaded')
) {
const automedia = frigateCardCarousel.getCarouselPlugins()?.autoMedia;
if (automedia) {
// If this has changed to preloaded (i.e. is now loaded but in the
// background) take the appropriate play/pause/mute/unmute actions.
@@ -257,8 +259,8 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
automedia.mute();
}
} else {
this._autoPlayHandler();
this._autoUnmuteHandler();
frigateCardMediaCarousel.autoPlay();
frigateCardMediaCarousel.autoUnmute();
}
}
}
@@ -268,8 +270,11 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
* Get the transition effect to use.
* @returns An TransitionEffect object.
*/
protected _getTransitionEffect(): TransitionEffect | undefined {
return this.liveConfig?.transition_effect;
protected _getTransitionEffect(): TransitionEffect {
return (
this.liveConfig?.transition_effect ??
frigateCardConfigDefaults.live.transition_effect
);
}
/**
@@ -293,7 +298,6 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
*/
protected _getPlugins(): EmblaPluginType[] {
return [
...super._getPlugins(),
// Only enable wheel plugin if there is more than one camera.
...(this.cameras && this.cameras.size > 1
? [
@@ -314,6 +318,8 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
lazyUnloadCallback: (index, slide) =>
this._lazyloadOrUnloadSlide('unload', index, slide),
}),
// TODO: AutoMediaPlugin could be moved to MediaCarousel.
AutoMediaPlugin({
playerSelector: 'frigate-card-live-provider',
...(this.liveConfig?.auto_play && {
@@ -332,30 +338,6 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
];
}
/**
* Play the media on the loaded slide.
*/
protected _autoPlayHandler(): void {
if (
this.liveConfig?.auto_play &&
['all', 'selected'].includes(this.liveConfig.auto_play)
) {
super._autoPlayHandler();
}
}
/**
* Unmute the media on the loaded slide.
*/
protected _autoUnmuteHandler(): void {
if (
this.liveConfig?.auto_unmute &&
['all', 'selected'].includes(this.liveConfig.auto_unmute)
) {
super._autoUnmuteHandler();
}
}
/**
* Returns the number of slides to lazily load. 0 means all slides are lazy
* loaded, 1 means that 1 slide on each side of the currently selected slide
@@ -394,15 +376,17 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
/**
* Handle the user selecting a new slide in the carousel.
*/
protected _selectSlideSetViewHandler(): void {
if (!this._carousel || !this.view || !this.cameras) {
protected _setViewHandler(): void {
const selectedCameraIndex = this._refMediaCarousel.value
?.frigateCardCarousel()
?.carouselSelected();
if (selectedCameraIndex === undefined || !this.view || !this.cameras) {
return;
}
const selectedSnap = this._carousel.selectedScrollSnap();
this.view
.evolve({
camera: Array.from(this.cameras.keys())[selectedSnap],
camera: Array.from(this.cameras.keys())[selectedCameraIndex],
// Reset the target so thumbnails will be re-fetched.
target: null,
@@ -419,9 +403,13 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
protected _lazyloadOrUnloadSlide(
action: 'load' | 'unload',
_index: number,
slide: HTMLElement,
slide: Element,
): void {
const liveProvider = slide.querySelector(
if (slide instanceof HTMLSlotElement) {
slide = slide.assignedElements({flatten: true})[0];
}
const liveProvider = slide?.querySelector(
'frigate-card-live-provider',
) as FrigateCardLiveProvider;
if (liveProvider) {
@@ -451,18 +439,21 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
conditionState,
) as LiveConfig;
return html` <div class="embla__slide">
<frigate-card-live-provider
?disabled=${this.liveConfig.lazy_load}
.cameraConfig=${cameraConfig}
.label=${getCameraTitle(this.hass, cameraConfig)}
.liveConfig=${config}
.hass=${this.hass}
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) =>
this._mediaShowEventHandler(slideIndex, e)}
>
</frigate-card-live-provider>
</div>`;
return html`
<div class="embla__slide">
<frigate-card-live-provider
?disabled=${this.liveConfig.lazy_load}
.cameraConfig=${cameraConfig}
.label=${getCameraTitle(this.hass, cameraConfig)}
.liveConfig=${config}
.hass=${this.hass}
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) => {
wrapMediaShowEventForCarousel(slideIndex, e)
}}
>
</frigate-card-live-provider>
</div>
`;
}
protected _getCameraNeighbors(): [CameraConfig | null, CameraConfig | null] {
@@ -487,30 +478,6 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
return [prev, next];
}
/**
* Handle updating of the next/previous controls when the carousel is moved.
*/
protected _selectSlideNextPreviousHandler(): void {
const updateNextPreviousControl = (
control: FrigateCardNextPreviousControl,
direction: 'previous' | 'next',
): void => {
const [prev, next] = this._getCameraNeighbors();
const target = direction == 'previous' ? prev : next;
control.disabled = target == null;
control.title = getCameraTitle(this.hass, target);
control.icon = getCameraIcon(this.hass, target);
};
if (this._previousControlRef.value) {
updateNextPreviousControl(this._previousControlRef.value, 'previous');
}
if (this._nextControlRef.value) {
updateNextPreviousControl(this._nextControlRef.value, 'next');
}
}
/**
* Render the element.
* @returns A template to display to the user.
@@ -532,46 +499,58 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
const title = getCameraTitle(this.hass, this.cameras.get(this.view.camera));
return html`
<div class="embla">
<frigate-card-media-carousel
${ref(this._refMediaCarousel)}
.carouselOptions=${this._getOptions()}
.carouselPlugins=${this._getPlugins()}
.autoPlayCondition=${this.liveConfig.auto_play}
.autoPauseCondition=${this.liveConfig.auto_pause}
.autoMuteCondition=${this.liveConfig.auto_mute}
.autoUnmuteCondition=${this.liveConfig.auto_unmute}
.label="${title ? `${localize('common.live')}: ${title}` : ''}"
.titlePopupConfig=${config.controls.title}
transitionEffect=${this._getTransitionEffect()}
@frigate-card:carousel:select=${this._setViewHandler.bind(this)}
>
<frigate-card-next-previous-control
${ref(this._previousControlRef)}
slot="previous"
.direction=${'previous'}
.controlConfig=${config.controls.next_previous}
.label=${getCameraTitle(this.hass, prev)}
.icon=${getCameraIcon(this.hass, prev)}
?disabled=${prev == null}
@click=${(ev) => {
this._nextPreviousHandler('previous');
this._refMediaCarousel.value
?.frigateCardCarousel()
?.carouselScrollPrevious();
stopEventFromActivatingCardWideActions(ev);
}}
>
</frigate-card-next-previous-control>
<div class="embla__viewport">
<div class="embla__container">${slides}</div>
</div>
${slides}
<frigate-card-next-previous-control
${ref(this._nextControlRef)}
slot="next"
.direction=${'next'}
.controlConfig=${config.controls.next_previous}
.label=${getCameraTitle(this.hass, next)}
.icon=${getCameraIcon(this.hass, next)}
?disabled=${next == null}
@click=${(ev) => {
this._nextPreviousHandler('next');
this._refMediaCarousel.value?.frigateCardCarousel()?.carouselScrollNext();
stopEventFromActivatingCardWideActions(ev);
}}
>
</frigate-card-next-previous-control>
</div>
<frigate-card-title-control
${ref(this._titleControlRef)}
.config=${config.controls.title}
.text="${title ? `${localize('common.live')}: ${title}` : ''}"
.fitInto=${this as HTMLElement}
>
</frigate-card-title-control>
</frigate-card-media-carousel>
`;
}
/**
* Get styles.
*/
static get styles(): CSSResultGroup {
return unsafeCSS(liveCarouselStyle);
}
}
@customElement('frigate-card-live-provider')
@@ -747,20 +726,16 @@ export class FrigateCardLiveFrigate extends LitElement {
}
if (!this.cameraConfig?.camera_entity) {
return dispatchErrorMessageEvent(
this,
localize('error.no_live_camera'),
{ context: this.cameraConfig },
);
return dispatchErrorMessageEvent(this, localize('error.no_live_camera'), {
context: this.cameraConfig,
});
}
const stateObj = this.hass.states[this.cameraConfig.camera_entity];
if (!stateObj || stateObj.state === 'unavailable') {
return dispatchErrorMessageEvent(
this,
localize('error.live_camera_unavailable'),
{ context: this.cameraConfig },
);
return dispatchErrorMessageEvent(this, localize('error.live_camera_unavailable'), {
context: this.cameraConfig,
});
}
return html` <frigate-card-ha-camera-stream
@@ -1143,11 +1118,9 @@ export class FrigateCardLiveJSMPEG extends LitElement {
this._jsmpegCanvasElement.className = 'media';
if (!this.cameraConfig?.frigate.camera_name) {
return dispatchErrorMessageEvent(
this,
localize('error.no_camera_name'),
{ context: this.cameraConfig },
);
return dispatchErrorMessageEvent(this, localize('error.no_camera_name'), {
context: this.cameraConfig,
});
}
const url = await this._getURL();