Add controls for live carousel.
This commit is contained in:
+1
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
+110
-42
@@ -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 {
|
||||
</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.
|
||||
* @returns A template to display to the user.
|
||||
@@ -289,42 +356,43 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
||||
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>`
|
||||
// : ``}
|
||||
const [prev, next] = this._getCameraNeighbors();
|
||||
|
||||
return html`<div class="embla">
|
||||
<div class="embla__viewport">
|
||||
<div class="embla__container">${slides}</div>
|
||||
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__container">${slides}</div>
|
||||
</div>
|
||||
<frigate-card-next-previous-control
|
||||
${ref(this._nextControlRef)}
|
||||
.direction=${'next'}
|
||||
.controlConfig=${{
|
||||
style: 'chevrons',
|
||||
size: '40px',
|
||||
}}
|
||||
.title=${next && next.title ? next.title : ''}
|
||||
?disabled=${next == null}
|
||||
@click=${() => {
|
||||
this._nextPreviousHandler('next');
|
||||
}}
|
||||
>
|
||||
</frigate-card-next-previous-control>
|
||||
</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>`
|
||||
// : ``}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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
|
||||
* 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.
|
||||
|
||||
@@ -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``;
|
||||
}
|
||||
|
||||
|
||||
+46
-30
@@ -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`<div class="embla">
|
||||
${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>`
|
||||
: ``}
|
||||
<frigate-card-next-previous-control
|
||||
${ref(this._previousControlRef)}
|
||||
.direction=${'previous'}
|
||||
.controlConfig=${this.viewerConfig?.controls.next_previous}
|
||||
.thumbnail=${prev && prev.thumbnail ? prev.thumbnail : undefined}
|
||||
.title=${prev ? prev.title : ''}
|
||||
?disabled=${!prev}
|
||||
@click=${() => {
|
||||
this._nextPreviousHandler('previous');
|
||||
}}
|
||||
></frigate-card-next-previous-control>
|
||||
<div class="embla__viewport">
|
||||
<div class="embla__container">${slides}</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>`
|
||||
: ``}
|
||||
<frigate-card-next-previous-control
|
||||
${ref(this._nextControlRef)}
|
||||
.direction=${'next'}
|
||||
.controlConfig=${this.viewerConfig?.controls.next_previous}
|
||||
.thumbnail=${next && next.thumbnail ? next.thumbnail : undefined}
|
||||
.title=${next ? next.title : ''}
|
||||
?disabled=${!next}
|
||||
@click=${() => {
|
||||
this._nextPreviousHandler('next');
|
||||
}}
|
||||
></frigate-card-next-previous-control>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,4 @@
|
||||
|
||||
.embla__slide {
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
Reference in New Issue
Block a user