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');
+ }}
+ >
+
+
+
{
+ 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');
+ }}
+ >
- ${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