Add controls for live carousel.

This commit is contained in:
Dermot Duffy
2022-01-14 21:31:16 -08:00
parent c83299a2e6
commit 4a93d02fe9
6 changed files with 182 additions and 82 deletions
+108 -40
View File
@@ -1,5 +1,4 @@
// TODO height adapting // TODO title on hover over camera
// TODO can height adapting remove need for 16x9 dummy in the media carousel?
// TODO controls // TODO controls
// TODO lazy loading configuration // TODO lazy loading configuration
// TODO editor // TODO editor
@@ -15,14 +14,17 @@ import type {
LiveConfig, LiveConfig,
MediaShowInfo, MediaShowInfo,
WebRTCConfig, WebRTCConfig,
StateParameters,
} from '../types.js'; } from '../types.js';
import { EmblaOptionsType } from 'embla-carousel'; import { 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 { ref } from 'lit/directives/ref';
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 { FrigateCardMediaCarousel } from './media-carousel.js'; import { FrigateCardMediaCarousel } from './media-carousel.js';
import { FrigateCardNextPreviousControl } from './next-prev-control.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 +36,7 @@ import {
dispatchPauseEvent, dispatchPauseEvent,
dispatchPlayEvent, dispatchPlayEvent,
homeAssistantSignPath, homeAssistantSignPath,
refreshDynamicStateParameters,
} from '../common.js'; } from '../common.js';
import { renderProgressIndicator } from '../components/message.js'; import { renderProgressIndicator } from '../components/message.js';
@@ -166,7 +169,7 @@ export class FrigateCardLive extends LitElement {
@frigate-card:media-show=${this._mediaShowHandler} @frigate-card:media-show=${this._mediaShowHandler}
@frigate-card:carousel:select=${() => { @frigate-card:carousel:select=${() => {
// Re-rendering the component will cause the thumbnails to be // Re-rendering the component will cause the thumbnails to be
// re-fetched. // re-fetched (which is necessary because the camera has changed).
this.requestUpdate(); this.requestUpdate();
}} }}
> >
@@ -261,7 +264,7 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
'frigate-card-live-provider', 'frigate-card-live-provider',
) as FrigateCardLiveProvider; ) as FrigateCardLiveProvider;
if (liveProvider) { if (liveProvider) {
liveProvider.lazyLoad = false; liveProvider.disabled = false;
} }
} }
@@ -279,6 +282,70 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
</div>`; </div>`;
} }
protected _getCameraNeighbors(): [StateParameters | null, StateParameters | null] {
if (!this.cameras || !this.view || !this.hass) {
return [null, null];
}
const keys = Array.from(this.cameras.keys());
const currentIndex = keys.indexOf(this.view.camera);
if (currentIndex < 0) {
return [null, null];
}
const getDynamicParameters = (config: CameraConfig): CameraConfig | null => {
if (!this.hass) {
return null;
}
const stateParameters = refreshDynamicStateParameters(this.hass, {
entity: config.camera_entity,
title: config.title,
icon: config.icon,
});
return {
...config,
title: stateParameters.title ?? undefined,
icon: stateParameters.icon,
};
};
let prev: CameraConfig | null = null,
next: CameraConfig | null = null;
if (currentIndex > 0) {
prev = this.cameras.get(keys[currentIndex - 1]) ?? null;
if (prev) {
prev = getDynamicParameters(prev);
}
}
if (currentIndex + 1 < this.cameras.size) {
next = this.cameras.get(keys[currentIndex + 1]) ?? null;
if (next) {
next = getDynamicParameters(next);
}
}
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 = (target && target.title ? target.title : '')
}
if (this._previousControlRef.value) {
updateNextPreviousControl(this._previousControlRef.value, 'previous');
}
if (this._nextControlRef.value) {
updateNextPreviousControl(this._nextControlRef.value, 'next');
}
}
/** /**
* Render the element. * Render the element.
* @returns A template to display to the user. * @returns A template to display to the user.
@@ -289,42 +356,43 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
return; return;
} }
// ${neighbors && neighbors.previous const [prev, next] = this._getCameraNeighbors();
// ? 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"> return html`
<div class="embla">
<frigate-card-next-previous-control
${ref(this._previousControlRef)}
.direction=${'previous'}
.controlConfig=${{
style: 'chevrons',
size: '40px',
}}
.title=${prev && prev.title ? prev.title : ''}
?disabled=${prev == null}
@click=${() => {
this._nextPreviousHandler('previous');
}}
>
</frigate-card-next-previous-control>
<div class="embla__viewport"> <div class="embla__viewport">
<div class="embla__container">${slides}</div> <div class="embla__container">${slides}</div>
</div> </div>
</div>`; <frigate-card-next-previous-control
// ${neighbors && neighbors.next ${ref(this._nextControlRef)}
// ? html`<frigate-card-next-previous-control .direction=${'next'}
// .direction=${'next'} .controlConfig=${{
// .controlConfig=${this.viewerConfig?.controls.next_previous} style: 'chevrons',
// .thumbnail=${neighbors.next.thumbnail} size: '40px',
// .title=${neighbors.next.title} }}
// .actionHandler=${actionHandler({ .title=${next && next.title ? next.title : ''}
// hasHold: false, ?disabled=${next == null}
// hasDoubleClick: false, @click=${() => {
// })} this._nextPreviousHandler('next');
// @action=${() => { }}
// this._nextPreviousHandler('next'); >
// }} </frigate-card-next-previous-control>
// ></frigate-card-next-previous-control>` </div>
// : ``} `;
} }
} }
@@ -339,17 +407,17 @@ export class FrigateCardLiveProvider extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected liveConfig?: LiveConfig; protected liveConfig?: LiveConfig;
// Whether or not to lazy load this slide. If `true`, no contents are rendered // Whether or not to disable this entity. If `true`, no contents are rendered
// until this attribute is set to `false`. // until this attribute is set to `false` (this is useful for lazy loading).
@property({ attribute: true, type: Boolean }) @property({ attribute: true, type: Boolean })
public lazyLoad = false; public disabled = false;
/** /**
* Master render method. * Master render method.
* @returns A rendered template. * @returns A rendered template.
*/ */
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
if (this.lazyLoad || !this.hass || !this.liveConfig || !this.cameraConfig) { if (this.disabled || !this.hass || !this.liveConfig || !this.cameraConfig) {
return; return;
} }
+16
View File
@@ -1,5 +1,6 @@
import { CSSResultGroup, unsafeCSS } from 'lit'; import { CSSResultGroup, unsafeCSS } from 'lit';
import { EmblaCarouselType } from 'embla-carousel'; import { EmblaCarouselType } from 'embla-carousel';
import { createRef, Ref } from 'lit/directives/ref';
import { customElement } from 'lit/decorators.js'; import { customElement } from 'lit/decorators.js';
import { FrigateCardCarousel } from './carousel.js'; import { FrigateCardCarousel } from './carousel.js';
@@ -13,6 +14,8 @@ import './next-prev-control.js';
import mediaCarouselStyle from '../scss/media-carousel.scss'; import mediaCarouselStyle from '../scss/media-carousel.scss';
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
const getEmptyImageSrc = (width: number, height: number) => const getEmptyImageSrc = (width: number, height: number) =>
`data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`; `data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`;
export const IMG_EMPTY = getEmptyImageSrc(16, 9); export const IMG_EMPTY = getEmptyImageSrc(16, 9);
@@ -25,6 +28,9 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
// Whether or not a given slide has been successfully lazily loaded. // Whether or not a given slide has been successfully lazily loaded.
protected _slideHasBeenLazyLoaded: Record<number, boolean> = {}; protected _slideHasBeenLazyLoaded: Record<number, boolean> = {};
protected _nextControlRef: Ref<FrigateCardNextPreviousControl> = createRef();
protected _previousControlRef: Ref<FrigateCardNextPreviousControl> = createRef();
/** /**
* Returns the number of slides to lazily load. 0 means all slides are lazy * 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 * loaded, 1 means that 1 slide on each side of the currently selected slide
@@ -50,6 +56,9 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
// Update the view object as the carousel is moved. // Update the view object as the carousel is moved.
carousel?.on('select', this._selectSlideSetViewHandler.bind(this)); carousel?.on('select', this._selectSlideSetViewHandler.bind(this));
// Update the next/previous controls as the carousel is moved.
carousel?.on('select', this._selectSlideNextPreviousHandler.bind(this));
// Dispatch MediaShow events as the carousel is moved. // Dispatch MediaShow events as the carousel is moved.
carousel?.on('init', this._selectSlideMediaShowHandler.bind(this)); carousel?.on('init', this._selectSlideMediaShowHandler.bind(this));
carousel?.on('select', this._selectSlideMediaShowHandler.bind(this)); carousel?.on('select', this._selectSlideMediaShowHandler.bind(this));
@@ -96,6 +105,13 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
// To be overridden in children. // To be overridden in children.
} }
/**
* Handle updating of the next/previous controls when the carousel is moved.
*/
protected _selectSlideNextPreviousHandler(): void {
// To be overridden in children.
}
/** /**
* Handle a next/previous control interaction. * Handle a next/previous control interaction.
* @param direction The direction requested, previous or next. * @param direction The direction requested, previous or next.
+9 -3
View File
@@ -8,8 +8,11 @@ import controlStyle from '../scss/next-previous-control.scss';
@customElement('frigate-card-next-previous-control') @customElement('frigate-card-next-previous-control')
export class FrigateCardNextPreviousControl extends LitElement { export class FrigateCardNextPreviousControl extends LitElement {
@property({ attribute: true })
public title = '';
@property({ attribute: false }) @property({ attribute: false })
protected direction?: 'next' | 'previous'; public direction?: 'next' | 'previous';
@property({ attribute: false }) @property({ attribute: false })
set controlConfig(controlConfig: NextPreviousControlConfig | undefined) { set controlConfig(controlConfig: NextPreviousControlConfig | undefined) {
@@ -21,10 +24,13 @@ export class FrigateCardNextPreviousControl extends LitElement {
protected _controlConfig?: NextPreviousControlConfig; protected _controlConfig?: NextPreviousControlConfig;
@property({ attribute: false }) @property({ attribute: false })
protected thumbnail?: string; public thumbnail?: string;
@property({ attribute: true, type: Boolean })
public disabled = false;
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._controlConfig || this._controlConfig.style == 'none') { if (this.disabled || !this._controlConfig || this._controlConfig.style == 'none') {
return html``; return html``;
} }
+38 -22
View File
@@ -16,6 +16,7 @@ import type {
ViewerConfig, ViewerConfig,
} from '../types.js'; } from '../types.js';
import { FrigateCardMediaCarousel, IMG_EMPTY } from './media-carousel.js'; import { FrigateCardMediaCarousel, IMG_EMPTY } from './media-carousel.js';
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
import { FrigateCardThumbnailCarousel, ThumbnailCarouselTap } from './thumbnail-carousel.js'; import { FrigateCardThumbnailCarousel, ThumbnailCarouselTap } from './thumbnail-carousel.js';
import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js'; import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js';
import { View } from '../view.js'; import { View } from '../view.js';
@@ -436,6 +437,28 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
} }
} }
/**
* Handle updating of the next/previous controls when the carousel is moved.
*/
protected _selectSlideNextPreviousHandler(): void {
const updateNextPreviousControl = (control: FrigateCardNextPreviousControl, direction: 'previous' | 'next'): void => {
const neighbors = this._getMediaNeighbors();
const [prev, next] = [neighbors?.previous, neighbors?.next]
const target = direction == 'previous' ? prev : next;
control.disabled = (target == null)
control.title = (target && target.title ? target.title : '')
control.thumbnail = (target && target.thumbnail ? target.thumbnail : undefined)
}
if (this._previousControlRef.value) {
updateNextPreviousControl(this._previousControlRef.value, 'previous');
}
if (this._nextControlRef.value) {
updateNextPreviousControl(this._nextControlRef.value, 'next');
}
}
/** /**
* Get slides to include in the render. * Get slides to include in the render.
* @returns The slides to include in the render. * @returns The slides to include in the render.
@@ -474,41 +497,34 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
} }
const neighbors = this._getMediaNeighbors(); const neighbors = this._getMediaNeighbors();
const [prev, next] = [neighbors?.previous, neighbors?.next]
return html`<div class="embla"> return html`<div class="embla">
${neighbors && neighbors.previous <frigate-card-next-previous-control
? html`<frigate-card-next-previous-control ${ref(this._previousControlRef)}
.direction=${'previous'} .direction=${'previous'}
.controlConfig=${this.viewerConfig?.controls.next_previous} .controlConfig=${this.viewerConfig?.controls.next_previous}
.thumbnail=${neighbors.previous.thumbnail} .thumbnail=${prev && prev.thumbnail ? prev.thumbnail : undefined}
.title=${neighbors.previous.title} .title=${prev ? prev.title : ''}
.actionHandler=${actionHandler({ ?disabled=${!prev}
hasHold: false, @click=${() => {
hasDoubleClick: false,
})}
@action=${() => {
this._nextPreviousHandler('previous'); this._nextPreviousHandler('previous');
}} }}
></frigate-card-next-previous-control>` ></frigate-card-next-previous-control>
: ``}
<div class="embla__viewport"> <div class="embla__viewport">
<div class="embla__container">${slides}</div> <div class="embla__container">${slides}</div>
</div> </div>
${neighbors && neighbors.next <frigate-card-next-previous-control
? html`<frigate-card-next-previous-control ${ref(this._nextControlRef)}
.direction=${'next'} .direction=${'next'}
.controlConfig=${this.viewerConfig?.controls.next_previous} .controlConfig=${this.viewerConfig?.controls.next_previous}
.thumbnail=${neighbors.next.thumbnail} .thumbnail=${next && next.thumbnail ? next.thumbnail : undefined}
.title=${neighbors.next.title} .title=${next ? next.title : ''}
.actionHandler=${actionHandler({ ?disabled=${!next}
hasHold: false, @click=${() => {
hasDoubleClick: false,
})}
@action=${() => {
this._nextPreviousHandler('next'); this._nextPreviousHandler('next');
}} }}
></frigate-card-next-previous-control>` ></frigate-card-next-previous-control>
: ``}
</div>`; </div>`;
} }
-6
View File
@@ -5,9 +5,3 @@
.embla__slide { .embla__slide {
flex: 0 0 100%; flex: 0 0 100%;
} }
* {
height: 100%;
width: 100%;
transform-style: preserve-3d;
}