From 4a93d02fe95f69c7ca6921379303c917e3017160 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Wed, 22 Dec 2021 12:15:48 -0800 Subject: [PATCH] Add controls for live carousel. --- src/common.ts | 2 +- src/components/live.ts | 152 ++++++++++++++++++++-------- src/components/media-carousel.ts | 16 +++ src/components/next-prev-control.ts | 12 ++- src/components/viewer.ts | 76 ++++++++------ src/scss/media-carousel.scss | 6 -- 6 files changed, 182 insertions(+), 82 deletions(-) diff --git a/src/common.ts b/src/common.ts index e1ea28de..887bdee2 100644 --- a/src/common.ts +++ b/src/common.ts @@ -394,4 +394,4 @@ export function refreshDynamicStateParameters( params.title = params.title ?? (state?.attributes?.friendly_name || params.entity); params.icon = params.icon ?? stateIcon(state); return params; -} +} \ No newline at end of file diff --git a/src/components/live.ts b/src/components/live.ts index ea8e403a..0cd2336a 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -1,5 +1,4 @@ -// TODO height adapting -// TODO can height adapting remove need for 16x9 dummy in the media carousel? +// TODO title on hover over camera // TODO controls // TODO lazy loading configuration // TODO editor @@ -15,14 +14,17 @@ import type { LiveConfig, MediaShowInfo, WebRTCConfig, + StateParameters, } from '../types.js'; import { EmblaOptionsType } from 'embla-carousel'; import { HomeAssistant } from 'custom-card-helpers'; import { customElement, property } from 'lit/decorators.js'; +import { ref } from 'lit/directives/ref'; import { until } from 'lit/directives/until.js'; import { BrowseMediaUtil } from '../browse-media-util.js'; import { FrigateCardMediaCarousel } from './media-carousel.js'; +import { FrigateCardNextPreviousControl } from './next-prev-control.js'; import { ThumbnailCarouselTap } from './thumbnail-carousel.js'; import { View } from '../view.js'; import { localize } from '../localize/localize.js'; @@ -34,6 +36,7 @@ import { dispatchPauseEvent, dispatchPlayEvent, homeAssistantSignPath, + refreshDynamicStateParameters, } from '../common.js'; import { renderProgressIndicator } from '../components/message.js'; @@ -166,7 +169,7 @@ export class FrigateCardLive extends LitElement { @frigate-card:media-show=${this._mediaShowHandler} @frigate-card:carousel:select=${() => { // Re-rendering the component will cause the thumbnails to be - // re-fetched. + // re-fetched (which is necessary because the camera has changed). this.requestUpdate(); }} > @@ -261,7 +264,7 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { 'frigate-card-live-provider', ) as FrigateCardLiveProvider; if (liveProvider) { - liveProvider.lazyLoad = false; + liveProvider.disabled = false; } } @@ -279,6 +282,70 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { `; } + 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. * @returns A template to display to the user. @@ -289,42 +356,43 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { return; } - // ${neighbors && neighbors.previous - // ? html` { - // this._nextPreviousHandler('previous'); - // }} - // >` - // : ``} + const [prev, next] = this._getCameraNeighbors(); - return html`
-
-
${slides}
+ return html` +
+ { + this._nextPreviousHandler('previous'); + }} + > + +
+
${slides}
+
+ { + this._nextPreviousHandler('next'); + }} + > +
-
`; - // ${neighbors && neighbors.next - // ? html` { - // this._nextPreviousHandler('next'); - // }} - // >` - // : ``} + `; } } @@ -339,17 +407,17 @@ export class FrigateCardLiveProvider extends LitElement { @property({ attribute: false }) protected liveConfig?: LiveConfig; - // Whether or not to lazy load this slide. If `true`, no contents are rendered - // until this attribute is set to `false`. + // Whether or not to disable this entity. If `true`, no contents are rendered + // until this attribute is set to `false` (this is useful for lazy loading). @property({ attribute: true, type: Boolean }) - public lazyLoad = false; + public disabled = false; /** * Master render method. * @returns A rendered template. */ protected render(): TemplateResult | void { - if (this.lazyLoad || !this.hass || !this.liveConfig || !this.cameraConfig) { + if (this.disabled || !this.hass || !this.liveConfig || !this.cameraConfig) { return; } diff --git a/src/components/media-carousel.ts b/src/components/media-carousel.ts index 90f6a629..de31a1df 100644 --- a/src/components/media-carousel.ts +++ b/src/components/media-carousel.ts @@ -1,5 +1,6 @@ import { CSSResultGroup, unsafeCSS } from 'lit'; import { EmblaCarouselType } from 'embla-carousel'; +import { createRef, Ref } from 'lit/directives/ref'; import { customElement } from 'lit/decorators.js'; import { FrigateCardCarousel } from './carousel.js'; @@ -13,6 +14,8 @@ import './next-prev-control.js'; import mediaCarouselStyle from '../scss/media-carousel.scss'; +import { FrigateCardNextPreviousControl } from './next-prev-control.js'; + 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`; 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. protected _slideHasBeenLazyLoaded: Record = {}; + protected _nextControlRef: Ref = createRef(); + protected _previousControlRef: Ref = createRef(); + /** * 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 @@ -50,6 +56,9 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { // Update the view object as the carousel is moved. 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. carousel?.on('init', 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. } + /** + * 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. * @param direction The direction requested, previous or next. diff --git a/src/components/next-prev-control.ts b/src/components/next-prev-control.ts index e9e3d68b..f05529e6 100644 --- a/src/components/next-prev-control.ts +++ b/src/components/next-prev-control.ts @@ -8,8 +8,11 @@ import controlStyle from '../scss/next-previous-control.scss'; @customElement('frigate-card-next-previous-control') export class FrigateCardNextPreviousControl extends LitElement { + @property({ attribute: true }) + public title = ''; + @property({ attribute: false }) - protected direction?: 'next' | 'previous'; + public direction?: 'next' | 'previous'; @property({ attribute: false }) set controlConfig(controlConfig: NextPreviousControlConfig | undefined) { @@ -21,10 +24,13 @@ export class FrigateCardNextPreviousControl extends LitElement { protected _controlConfig?: NextPreviousControlConfig; @property({ attribute: false }) - protected thumbnail?: string; + public thumbnail?: string; + + @property({ attribute: true, type: Boolean }) + public disabled = false; protected render(): TemplateResult { - if (!this._controlConfig || this._controlConfig.style == 'none') { + if (this.disabled || !this._controlConfig || this._controlConfig.style == 'none') { return html``; } diff --git a/src/components/viewer.ts b/src/components/viewer.ts index 0020b760..f8c51952 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -16,6 +16,7 @@ import type { ViewerConfig, } from '../types.js'; import { FrigateCardMediaCarousel, IMG_EMPTY } from './media-carousel.js'; +import { FrigateCardNextPreviousControl } from './next-prev-control.js'; import { FrigateCardThumbnailCarousel, ThumbnailCarouselTap } from './thumbnail-carousel.js'; import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.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. * @returns The slides to include in the render. @@ -474,41 +497,34 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { } const neighbors = this._getMediaNeighbors(); + const [prev, next] = [neighbors?.previous, neighbors?.next] return html`
- ${neighbors && neighbors.previous - ? html` { - this._nextPreviousHandler('previous'); - }} - >` - : ``} + { + this._nextPreviousHandler('previous'); + }} + >
${slides}
- ${neighbors && neighbors.next - ? html` { - this._nextPreviousHandler('next'); - }} - >` - : ``} + { + this._nextPreviousHandler('next'); + }} + >
`; } diff --git a/src/scss/media-carousel.scss b/src/scss/media-carousel.scss index b4a7dd1d..5611628f 100644 --- a/src/scss/media-carousel.scss +++ b/src/scss/media-carousel.scss @@ -4,10 +4,4 @@ .embla__slide { flex: 0 0 100%; -} - -* { - height: 100%; - width: 100%; - transform-style: preserve-3d; } \ No newline at end of file