Refactor carousel into separate components.
This commit is contained in:
+156
-38
@@ -1,10 +1,20 @@
|
||||
import EmblaCarousel, {
|
||||
EmblaCarouselType,
|
||||
EmblaOptionsType,
|
||||
EmblaPluginType
|
||||
} from 'embla-carousel';
|
||||
import { CSSResultGroup, LitElement, PropertyValues, unsafeCSS } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import EmblaCarousel, { EmblaCarouselType, EmblaOptionsType } from 'embla-carousel';
|
||||
import { EmblaNodesType } from 'embla-carousel/components';
|
||||
import {
|
||||
CreatePluginType,
|
||||
EmblaPluginsType,
|
||||
LoosePluginType,
|
||||
} from 'embla-carousel/components/Plugins';
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import carouselStyle from '../scss/carousel.scss';
|
||||
import { TransitionEffect } from '../types';
|
||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||
@@ -13,28 +23,120 @@ export interface CarouselSelect {
|
||||
index: number;
|
||||
}
|
||||
|
||||
export type EmblaCarouselPlugins = CreatePluginType<
|
||||
LoosePluginType,
|
||||
Record<string, unknown>
|
||||
>[];
|
||||
|
||||
@customElement('frigate-card-carousel')
|
||||
export class FrigateCardCarousel extends LitElement {
|
||||
@property({ attribute: true, reflect: true })
|
||||
public direction: 'vertical' | 'horizontal' = 'horizontal';
|
||||
|
||||
@property({ attribute: false })
|
||||
public carouselOptions?: EmblaOptionsType;
|
||||
|
||||
@property({ attribute: false })
|
||||
public carouselPlugins?: EmblaCarouselPlugins;
|
||||
|
||||
@property({ attribute: true })
|
||||
public transitionEffect?: TransitionEffect;
|
||||
|
||||
protected _refSlot: Ref<HTMLSlotElement> = createRef();
|
||||
|
||||
protected _carousel?: EmblaCarouselType;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
// Guarantee a re-render if the component is reconnected. See note in
|
||||
// disconnectedCallback().
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Component disconnected callback.
|
||||
*/
|
||||
disconnectedCallback(): void {
|
||||
// Destroy the carousel when the component is disconnected, which forces the
|
||||
// plugins (which may have registered event handlers) to also be destroyed.
|
||||
// The carousel will automatically reconstruct if the component is re-rendered.
|
||||
this._destroyCarousel();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to a particular slide.
|
||||
* @param index Slide number.
|
||||
*/
|
||||
carouselScrollTo(index: number): void {
|
||||
this._carousel?.scrollTo(index, this._getTransitionEffect() === 'none');
|
||||
public carouselScrollTo(index: number): void {
|
||||
this._carousel?.scrollTo(index, this.transitionEffect === 'none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the previous slide.
|
||||
*/
|
||||
public carouselScrollPrevious(): void {
|
||||
this._carousel?.scrollPrev(this.transitionEffect === 'none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the next slide.
|
||||
*/
|
||||
public carouselScrollNext(): void {
|
||||
this._carousel?.scrollNext(this.transitionEffect === 'none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected slide.
|
||||
* @returns The slide index or undefined if the carousel is not loaded.
|
||||
*/
|
||||
carouselSelected(): number | undefined {
|
||||
public carouselSelected(): number | undefined {
|
||||
return this._carousel?.selectedScrollSnap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected node.
|
||||
* @returns The slide index or undefined if the carousel is not loaded.
|
||||
*/
|
||||
public carouselSelectedElement(): HTMLElement | null {
|
||||
const selected = this._carousel?.selectedScrollSnap();
|
||||
if (selected !== undefined) {
|
||||
return this._carousel?.slideNodes()[selected] ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the carousel.
|
||||
*/
|
||||
public carouselClickAllowed(): boolean {
|
||||
return this._carousel?.clickAllowed() ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the carousel.
|
||||
*/
|
||||
public carousel(): EmblaCarouselType | null {
|
||||
return this._carousel ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ReInit the carousel.
|
||||
*/
|
||||
public carouselReInit(): void {
|
||||
// Safari appears to not loop the carousel unless the options are passed
|
||||
// back in during re-initialization.
|
||||
return this._carousel?.reInit(this.carouselOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the live carousel plugins.
|
||||
*/
|
||||
public getCarouselPlugins(): EmblaPluginsType | null {
|
||||
return this._carousel?.plugins() ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The updated lifecycle callback for this element.
|
||||
* @param changedProperties The properties that were changed in this render.
|
||||
@@ -52,30 +154,6 @@ export class FrigateCardCarousel extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the transition effect to use.
|
||||
* @returns An TransitionEffect object.
|
||||
*/
|
||||
protected _getTransitionEffect(): TransitionEffect | undefined {
|
||||
return 'slide';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Embla options to use.
|
||||
* @returns An EmblaOptionsType object or undefined for no options.
|
||||
*/
|
||||
protected _getOptions(): EmblaOptionsType | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Embla plugins to use.
|
||||
* @returns A list of EmblaOptionsTypes.
|
||||
*/
|
||||
protected _getPlugins(): EmblaPluginType[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
protected _destroyCarousel(): void {
|
||||
if (this._carousel) {
|
||||
this._carousel.destroy();
|
||||
@@ -91,14 +169,21 @@ export class FrigateCardCarousel extends LitElement {
|
||||
'.embla__viewport',
|
||||
) as HTMLElement;
|
||||
|
||||
if (carouselNode) {
|
||||
const nodes: EmblaNodesType = {
|
||||
root: carouselNode,
|
||||
// As the slides are slotted, need to explicitly pull them out and pass
|
||||
// them to Embla.
|
||||
slides: this._refSlot.value?.assignedElements({ flatten: true }) as HTMLElement[],
|
||||
};
|
||||
|
||||
if (carouselNode && nodes.slides) {
|
||||
this._carousel = EmblaCarousel(
|
||||
carouselNode,
|
||||
nodes,
|
||||
{
|
||||
axis: this.direction == 'horizontal' ? 'x' : 'y',
|
||||
...this._getOptions(),
|
||||
...this.carouselOptions,
|
||||
},
|
||||
this._getPlugins() ?? [],
|
||||
this.carouselPlugins,
|
||||
);
|
||||
this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init'));
|
||||
this._carousel.on('select', () => {
|
||||
@@ -112,6 +197,33 @@ export class FrigateCardCarousel extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the slotted children in the carousel change.
|
||||
*/
|
||||
protected _slotChanged(): void {
|
||||
// Cannot just re-init, because the slide elements themselves may have
|
||||
// changed, and only a carousel init can pass in new (slotted) children.
|
||||
this._destroyCarousel();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const slides = this._refSlot.value?.assignedElements({ flatten: true }) || [];
|
||||
const currentSlide = this._carousel?.selectedScrollSnap() ?? 0;
|
||||
const showPrevious = this.carouselOptions?.loop || currentSlide > 0;
|
||||
const showNext = this.carouselOptions?.loop || currentSlide + 1 < slides.length;
|
||||
|
||||
return html` <div class="embla">
|
||||
${showPrevious ? html`<slot name="previous"></slot>` : ``}
|
||||
<div class="embla__viewport">
|
||||
<div class="embla__container">
|
||||
<slot ${ref(this._refSlot)} @slotchange=${this._slotChanged}></slot>
|
||||
</div>
|
||||
</div>
|
||||
${showNext ? html`<slot name="next"></slot>` : ``}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element styles.
|
||||
*/
|
||||
@@ -119,3 +231,9 @@ export class FrigateCardCarousel extends LitElement {
|
||||
return unsafeCSS(carouselStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-carousel': FrigateCardCarousel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,12 @@ export type AutoMediaType = CreatePluginType<
|
||||
AutoMediaOptionsType
|
||||
>;
|
||||
|
||||
declare module 'embla-carousel/components/Plugins' {
|
||||
interface EmblaPluginsType {
|
||||
autoMedia?: AutoMediaType
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An Embla plugin to take automated actions on media (e.g. pause, unmute, etc).
|
||||
* @param userOptions
|
||||
@@ -112,7 +118,7 @@ export function AutoMediaPlugin(
|
||||
* Handle document visibility changes.
|
||||
*/
|
||||
function visibilityHandler(): void {
|
||||
if (document.visibilityState == 'hidden') {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
if (
|
||||
options.autoPauseCondition &&
|
||||
['all', 'hidden'].includes(options.autoPauseCondition)
|
||||
@@ -125,7 +131,7 @@ export function AutoMediaPlugin(
|
||||
) {
|
||||
muteAll();
|
||||
}
|
||||
} else if (document.visibilityState == 'visible') {
|
||||
} else if (document.visibilityState === 'visible') {
|
||||
if (
|
||||
options.autoPlayCondition &&
|
||||
['all', 'visible'].includes(options.autoPlayCondition)
|
||||
|
||||
@@ -19,7 +19,7 @@ export const defaultOptions: OptionsType = {
|
||||
lazyLoadCount: 0,
|
||||
};
|
||||
|
||||
export type LazyloadOptionsType = Partial<OptionsType>
|
||||
export type LazyloadOptionsType = Partial<OptionsType>;
|
||||
|
||||
export type LazyloadType = CreatePluginType<
|
||||
{
|
||||
@@ -28,6 +28,12 @@ export type LazyloadType = CreatePluginType<
|
||||
LazyloadOptionsType
|
||||
>;
|
||||
|
||||
declare module 'embla-carousel/components/Plugins' {
|
||||
interface EmblaPluginsType {
|
||||
lazyload?: LazyloadType;
|
||||
}
|
||||
}
|
||||
|
||||
export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType {
|
||||
const optionsHandler = EmblaCarousel.optionsHandler();
|
||||
const optionsBase = optionsHandler.merge(defaultOptions, Lazyload.globalOptions);
|
||||
|
||||
+93
-120
@@ -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();
|
||||
|
||||
+200
-128
@@ -1,17 +1,32 @@
|
||||
import { EmblaCarouselType } from 'embla-carousel';
|
||||
import { CSSResultGroup, unsafeCSS } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { createRef, Ref } from 'lit/directives/ref.js';
|
||||
// TODO: Use the auto-height plugin instead of adaptive height
|
||||
|
||||
import { EmblaOptionsType } from 'embla-carousel';
|
||||
import { EmblaPluginsType } from 'embla-carousel/components/Plugins';
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import mediaCarouselStyle from '../scss/media-carousel.scss';
|
||||
import type { MediaShowInfo } from '../types.js';
|
||||
import type {
|
||||
AutoMuteCondition,
|
||||
AutoPauseCondition,
|
||||
AutoPlayCondition,
|
||||
AutoUnmuteCondition,
|
||||
MediaShowInfo,
|
||||
NextPreviousControlConfig,
|
||||
TitleControlConfig,
|
||||
TransitionEffect,
|
||||
} from '../types.js';
|
||||
import { dispatchFrigateCardEvent } from '../utils/basic';
|
||||
import {
|
||||
createMediaShowInfo,
|
||||
dispatchExistingMediaShowInfoAsEvent,
|
||||
isValidMediaShowInfo
|
||||
isValidMediaShowInfo,
|
||||
} from '../utils/media-info.js';
|
||||
import { FrigateCardCarousel } from './carousel.js';
|
||||
import { EmblaCarouselPlugins, FrigateCardCarousel } from './carousel';
|
||||
import { AutoMediaType } from './embla-plugins/automedia.js';
|
||||
import { LazyloadType } from './embla-plugins/lazyload';
|
||||
import './next-prev-control.js';
|
||||
import './carousel.js';
|
||||
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
|
||||
import { FrigateCardTitleControl } from './title-control.js';
|
||||
|
||||
@@ -19,8 +34,90 @@ 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);
|
||||
|
||||
export interface CarouselMediaShowInfo {
|
||||
slide: number;
|
||||
mediaShowInfo: MediaShowInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a carousel media show event.
|
||||
* @param target The target to send it from.
|
||||
* @param carouselMediaShowInfo The CarouselMediaShowInfo.
|
||||
*/
|
||||
const dispatchFrigateCardCarouselMediaShow = (
|
||||
target: EventTarget,
|
||||
carouselMediaShowInfo: CarouselMediaShowInfo,
|
||||
): void => {
|
||||
dispatchFrigateCardEvent<CarouselMediaShowInfo>(
|
||||
target,
|
||||
'carousel:media-show',
|
||||
carouselMediaShowInfo,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Turn a MediaShowEvent into a CarouselMediaShowInfo.
|
||||
* @param slide The slide number.
|
||||
* @param event The MediaShowEvent.
|
||||
*/
|
||||
export const wrapMediaShowEventForCarousel = (
|
||||
slide: number,
|
||||
event: CustomEvent<MediaShowInfo>,
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
dispatchFrigateCardCarouselMediaShow(event.composedPath()[0], {
|
||||
slide: slide,
|
||||
mediaShowInfo: event.detail,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Turn a (stock) media load event into a CarouselMediaShowInfo.
|
||||
* @param slide The slide number.
|
||||
* @param event The MediaShowEvent.
|
||||
*/
|
||||
export const wrapMediaLoadEventForCarousel = (slide: number, event: Event) => {
|
||||
const mediaShowInfo = createMediaShowInfo(event);
|
||||
if (mediaShowInfo) {
|
||||
dispatchFrigateCardCarouselMediaShow(event.composedPath()[0], {
|
||||
slide: slide,
|
||||
mediaShowInfo: mediaShowInfo,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@customElement('frigate-card-media-carousel')
|
||||
export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
export class FrigateCardMediaCarousel extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public nextPreviousConfig?: NextPreviousControlConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public carouselOptions?: EmblaOptionsType;
|
||||
|
||||
@property({ attribute: false })
|
||||
public carouselPlugins?: EmblaCarouselPlugins;
|
||||
|
||||
@property({ attribute: true })
|
||||
public transitionEffect?: TransitionEffect;
|
||||
|
||||
@property({ attribute: false })
|
||||
public label?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public titlePopupConfig?: TitleControlConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public autoPlayCondition?: AutoPlayCondition;
|
||||
|
||||
@property({ attribute: false })
|
||||
public autoUnmuteCondition?: AutoUnmuteCondition;
|
||||
|
||||
@property({ attribute: false })
|
||||
public autoPauseCondition?: AutoPauseCondition;
|
||||
|
||||
@property({ attribute: false })
|
||||
public autoMuteCondition?: AutoMuteCondition;
|
||||
|
||||
// A "map" from slide number to MediaShowInfo object.
|
||||
protected _mediaShowInfo: Record<number, MediaShowInfo> = {};
|
||||
protected _nextControlRef: Ref<FrigateCardNextPreviousControl> = createRef();
|
||||
@@ -28,12 +125,19 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
protected _titleControlRef: Ref<FrigateCardTitleControl> = createRef();
|
||||
protected _titleTimerID: number | null = null;
|
||||
|
||||
protected _boundAutoPlayHandler = this.autoPlay.bind(this);
|
||||
protected _boundAutoPauseHandler = this.autoPause.bind(this);
|
||||
protected _boundAutoMuteHandler = this.autoMute.bind(this);
|
||||
protected _boundAutoUnmuteHandler = this.autoUnmute.bind(this);
|
||||
|
||||
// This carousel may be resized by Lovelace resizes, window resizes,
|
||||
// fullscreen, etc. Always call the adaptive height handler when the size
|
||||
// changes.
|
||||
protected _resizeObserver: ResizeObserver;
|
||||
protected _intersectionObserver: IntersectionObserver;
|
||||
|
||||
protected _refCarousel: Ref<FrigateCardCarousel> = createRef();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._resizeObserver = new ResizeObserver(this._adaptiveHeightHandler.bind(this));
|
||||
@@ -42,36 +146,61 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying carousel.
|
||||
*/
|
||||
public frigateCardCarousel(): FrigateCardCarousel | null {
|
||||
return this._refCarousel.value ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AutoMedia plugin (if any).
|
||||
* @returns The plugin or `null`.
|
||||
*/
|
||||
protected _getAutoMediaPlugin(): AutoMediaType | null {
|
||||
return this._carousel?.plugins()['autoMedia'] ?? null;
|
||||
return this.frigateCardCarousel()?.carousel()?.plugins().autoMedia ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the LazyLoad plugin (if any).
|
||||
* @returns The plugin or `null`.
|
||||
* Play the media on the selected slide.
|
||||
*/
|
||||
protected _getLazyLoadPlugin(): LazyloadType | null {
|
||||
return this._carousel?.plugins()['lazyload'] ?? null;
|
||||
public autoPlay(): void {
|
||||
if (this.autoPlayCondition && ['all', 'selected'].includes(this.autoPlayCondition)) {
|
||||
this._getAutoMediaPlugin()?.play();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the media on the selected slide. May be overridden to control when
|
||||
* autoplay should happen.
|
||||
* Pause the media on the selected slide.
|
||||
*/
|
||||
protected _autoPlayHandler(): void {
|
||||
this._getAutoMediaPlugin()?.play();
|
||||
public autoPause(): void {
|
||||
if (
|
||||
this.autoPauseCondition &&
|
||||
['all', 'selected'].includes(this.autoPauseCondition)
|
||||
) {
|
||||
this._getAutoMediaPlugin()?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmute the media on the selected slide. May be overridden to control when
|
||||
* autoplay should happen.
|
||||
* Unmute the media on the selected slide.
|
||||
*/
|
||||
protected _autoUnmuteHandler(): void {
|
||||
this._getAutoMediaPlugin()?.unmute();
|
||||
public autoUnmute(): void {
|
||||
if (
|
||||
this.autoUnmuteCondition &&
|
||||
['all', 'selected'].includes(this.autoUnmuteCondition)
|
||||
) {
|
||||
this._getAutoMediaPlugin()?.unmute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute the media on the selected slide.
|
||||
*/
|
||||
public autoMute(): void {
|
||||
if (this.autoMuteCondition && ['all', 'selected'].includes(this.autoMuteCondition)) {
|
||||
this._getAutoMediaPlugin()?.mute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,8 +224,7 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
|
||||
// Allow a brief pause after the media loads, but before the title is
|
||||
// displayed. This allows for a pleasant appearance/disappear of the title,
|
||||
// and allows for the browser to finish rendering the carousel (inc.
|
||||
// adaptive height which has `0.5s ease`, see `media-carousel.scss`).
|
||||
// and allows for the browser to finish rendering the carousel.
|
||||
this._titleTimerID = window.setTimeout(show, 0.5 * 1000);
|
||||
}
|
||||
|
||||
@@ -105,8 +233,8 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
*/
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener('frigate-card:media-show', this._autoPlayHandler);
|
||||
this.addEventListener('frigate-card:media-show', this._autoUnmuteHandler);
|
||||
this.addEventListener('frigate-card:media-show', this.autoPlay);
|
||||
this.addEventListener('frigate-card:media-show', this.autoUnmute);
|
||||
this.addEventListener('frigate-card:media-show', this._adaptiveHeightHandler);
|
||||
this.addEventListener('frigate-card:media-show', this._titleHandler);
|
||||
this._resizeObserver.observe(this);
|
||||
@@ -118,8 +246,8 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
*/
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener('frigate-card:media-show', this._autoPlayHandler);
|
||||
this.removeEventListener('frigate-card:media-show', this._autoUnmuteHandler);
|
||||
this.removeEventListener('frigate-card:media-show', this.autoPlay);
|
||||
this.removeEventListener('frigate-card:media-show', this.autoUnmute);
|
||||
this.removeEventListener('frigate-card:media-show', this._adaptiveHeightHandler);
|
||||
this.removeEventListener('frigate-card:media-show', this._titleHandler);
|
||||
this._resizeObserver.disconnect();
|
||||
@@ -143,9 +271,7 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
*/
|
||||
|
||||
const reInit = (): void => {
|
||||
// Safari appears to not loop the carousel unless the options are passed
|
||||
// back in during re-initialization.
|
||||
this._carousel?.reInit(this._getOptions());
|
||||
this.frigateCardCarousel()?.carouselReInit();
|
||||
};
|
||||
|
||||
if (entries.some((entry) => entry.isIntersecting)) {
|
||||
@@ -160,57 +286,21 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
}
|
||||
}
|
||||
|
||||
protected _destroyCarousel(): void {
|
||||
super._destroyCarousel();
|
||||
|
||||
// Notes on instance variables:
|
||||
// * this._mediaShowInfo: This is set when the media in the DOM loads. If a
|
||||
// new View included the same media, the DOM would not change and so the
|
||||
// prior contents would still be valid and would not re-appear (as the
|
||||
// media would not reload) -- as such, leave this alone on carousel
|
||||
// destroy. New media in that slide will replace the prior contents on
|
||||
// load.
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the carousel.
|
||||
*/
|
||||
protected _initCarousel(): void {
|
||||
super._initCarousel();
|
||||
|
||||
// Necessary because typescript local type narrowing is not paying attention
|
||||
// to the side-effect of the call to super._initCarousel().
|
||||
const carousel = this._carousel as EmblaCarouselType | undefined;
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the the height of the container on media load in case the dimensions
|
||||
* Set the the height of the component on media load in case the dimensions
|
||||
* have changed. This handler is not triggered from carousel events, as it's
|
||||
* actually the media load/show that will change the dimensions, and that is
|
||||
* async from carousel actions (e.g. lazy-loaded media).
|
||||
*/
|
||||
protected _adaptiveHeightHandler(): void {
|
||||
const adaptCarouselHeight = (): void => {
|
||||
if (!this._carousel) {
|
||||
return;
|
||||
}
|
||||
const slide = this._carousel?.selectedScrollSnap();
|
||||
const slide = this.frigateCardCarousel()?.carouselSelected();
|
||||
if (slide !== undefined) {
|
||||
this._carousel.containerNode().style.removeProperty('max-height');
|
||||
const slides = this._carousel.slideNodes();
|
||||
const height = slides[slide].getBoundingClientRect().height;
|
||||
if (height > 0) {
|
||||
this._carousel.containerNode().style.maxHeight = `${height}px`;
|
||||
this.style.removeProperty('max-height');
|
||||
const currentSlide = this.frigateCardCarousel()?.carouselSelectedElement();
|
||||
const height = currentSlide?.getBoundingClientRect().height;
|
||||
if (height !== undefined && height > 0) {
|
||||
this.style.maxHeight = `${height}px`;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -225,42 +315,12 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
window.requestAnimationFrame(adaptCarouselHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the user selecting a new slide in the carousel.
|
||||
*/
|
||||
protected _selectSlideSetViewHandler(): void {
|
||||
// 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.
|
||||
*/
|
||||
protected _nextPreviousHandler(direction: 'previous' | 'next'): void {
|
||||
if (direction === 'previous') {
|
||||
this._carousel?.scrollPrev(this._getTransitionEffect() === 'none');
|
||||
} else if (direction === 'next') {
|
||||
this._carousel?.scrollNext(this._getTransitionEffect() === 'none');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a media show event when a slide is selected.
|
||||
*/
|
||||
protected _selectSlideMediaShowHandler(): void {
|
||||
if (!this._carousel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slideIndex = this._carousel.selectedScrollSnap();
|
||||
if (slideIndex in this._mediaShowInfo) {
|
||||
protected _dispatchMediaShowInfo(): void {
|
||||
const slideIndex = this.frigateCardCarousel()?.carouselSelected();
|
||||
if (slideIndex !== undefined && slideIndex in this._mediaShowInfo) {
|
||||
dispatchExistingMediaShowInfoAsEvent(this, this._mediaShowInfo[slideIndex]);
|
||||
}
|
||||
}
|
||||
@@ -271,46 +331,58 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
* @param slideIndex The relevant slide index.
|
||||
* @param event The media-show event from the child component.
|
||||
*/
|
||||
protected _mediaShowEventHandler(
|
||||
slideIndex: number,
|
||||
event: CustomEvent<MediaShowInfo>,
|
||||
): void {
|
||||
protected _storeMediaShowInfo(event: CustomEvent<CarouselMediaShowInfo>): void {
|
||||
// Don't allow the inbound event to propagate upwards, that will be
|
||||
// automatically done at the appropriate time as the slide is shown.
|
||||
event.stopPropagation();
|
||||
this._mediaLoadedHandler(slideIndex, event.detail);
|
||||
}
|
||||
const mediaShowInfo = event.detail.mediaShowInfo;
|
||||
const slideIndex = event.detail.slide;
|
||||
|
||||
/**
|
||||
* Handle a MediaShowInfo object that is generated on media load, by saving it
|
||||
* for future, or immediate use, when the relevant slide is displayed.
|
||||
* @param slideIndex The relevant slide index.
|
||||
* @param mediaShowInfo The MediaShowInfo object generated by the media.
|
||||
*/
|
||||
protected _mediaLoadedHandler(
|
||||
slideIndex: number,
|
||||
mediaShowInfo?: MediaShowInfo | null,
|
||||
): void {
|
||||
// isValidMediaShowInfo is used to prevent saving media info that will be
|
||||
// rejected upstream (empty 1x1 images will be rejected here).
|
||||
if (mediaShowInfo && isValidMediaShowInfo(mediaShowInfo)) {
|
||||
this._mediaShowInfo[slideIndex] = mediaShowInfo;
|
||||
if (this._carousel && this._carousel?.selectedScrollSnap() === slideIndex) {
|
||||
if (this.frigateCardCarousel()?.carouselSelected() === slideIndex) {
|
||||
dispatchExistingMediaShowInfoAsEvent(this, mediaShowInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html` <frigate-card-carousel
|
||||
${ref(this._refCarousel)}
|
||||
.carouselOptions=${this.carouselOptions}
|
||||
.carouselPlugins=${this.carouselPlugins}
|
||||
transitionEffect=${ifDefined(this.transitionEffect)}
|
||||
@frigate-card:carousel:init=${this._dispatchMediaShowInfo.bind(this)}
|
||||
@frigate-card:carousel:select=${this._dispatchMediaShowInfo.bind(this)}
|
||||
@frigate-card:carousel:media-show=${this._storeMediaShowInfo.bind(this)}
|
||||
>
|
||||
<slot slot="previous" name="previous"></slot>
|
||||
<slot></slot>
|
||||
<slot slot="next" name="next"></slot>
|
||||
</frigate-card-carousel>
|
||||
${this.label && this.titlePopupConfig
|
||||
? html`<frigate-card-title-control
|
||||
${ref(this._titleControlRef)}
|
||||
.config=${this.titlePopupConfig}
|
||||
.text="${this.label}"
|
||||
.fitInto=${this as HTMLElement}
|
||||
>
|
||||
</frigate-card-title-control> `
|
||||
: ``}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return [super.styles, unsafeCSS(mediaCarouselStyle)];
|
||||
return unsafeCSS(mediaCarouselStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"frigate-card-media-carousel": FrigateCardMediaCarousel
|
||||
}
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-media-carousel': FrigateCardMediaCarousel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ export class FrigateCardSurround extends LitElement {
|
||||
.selected=${this.view.childIndex}
|
||||
.cameras=${this.cameras}
|
||||
@frigate-card:change-view=${(ev: CustomEvent) => changeDrawer(ev, 'close')}
|
||||
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
|
||||
@frigate-card:thumbnail-carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
|
||||
// Send the view change from the source of the tap event, so the
|
||||
// view change will be caught by the handler above (to close the drawer).
|
||||
this.view
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel';
|
||||
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
|
||||
import { CSSResultGroup, html, PropertyValues, TemplateResult, unsafeCSS } from 'lit';
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss';
|
||||
import type { CameraConfig, FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js';
|
||||
import {
|
||||
CameraConfig,
|
||||
FrigateBrowseMediaSource,
|
||||
ThumbnailsControlConfig,
|
||||
} from '../types.js';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||
import { isTrueMedia } from '../utils/ha/browse-media';
|
||||
import { View } from '../view.js';
|
||||
import { FrigateCardCarousel } from './carousel.js';
|
||||
import './thumbnail.js';
|
||||
import './carousel.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
|
||||
export interface ThumbnailCarouselTap {
|
||||
slideIndex: number;
|
||||
@@ -20,7 +34,7 @@ export interface ThumbnailCarouselTap {
|
||||
}
|
||||
|
||||
@customElement('frigate-card-thumbnail-carousel')
|
||||
export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
export class FrigateCardThumbnailCarousel extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@@ -35,6 +49,8 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
@property({ attribute: false })
|
||||
public cameras?: Map<string, CameraConfig>;
|
||||
|
||||
protected _refCarousel: Ref<FrigateCardCarousel> = createRef();
|
||||
|
||||
// Thumbnail carousels can expand (e.g. drawer-based carousels after the main
|
||||
// media loads). The carousel must be re-initialized in these cases, or the
|
||||
// dynamic sizing fails (and users can scroll past the end of the carousel).
|
||||
@@ -55,13 +71,7 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
set config(config: ThumbnailsControlConfig) {
|
||||
this.direction = ['left', 'right'].includes(config.mode) ? 'vertical' : 'horizontal';
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
@state()
|
||||
protected _config?: ThumbnailsControlConfig;
|
||||
public config?: ThumbnailsControlConfig;
|
||||
|
||||
@state()
|
||||
protected _selected?: number | null;
|
||||
@@ -70,13 +80,12 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
* Handle gallery resize.
|
||||
*/
|
||||
protected _resizeHandler(): void {
|
||||
if (this._carousel) {
|
||||
this._carousel.reInit();
|
||||
// Reinit will cause the scroll position to reset, so re-scroll to the
|
||||
// correct location.
|
||||
if (this._selected !== undefined && this._selected !== null) {
|
||||
this.carouselScrollTo(this._selected);
|
||||
}
|
||||
this._refCarousel.value?.carouselReInit();
|
||||
|
||||
// Reinit will cause the scroll position to reset, so re-scroll to the
|
||||
// correct location.
|
||||
if (this._selected !== undefined && this._selected !== null) {
|
||||
this._refCarousel.value?.carouselScrollTo(this._selected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +123,6 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
*/
|
||||
protected _getPlugins(): EmblaPluginType[] {
|
||||
return [
|
||||
...super._getPlugins(),
|
||||
// Only enable wheel plugin if there is more than one camera.
|
||||
WheelGesturesPlugin({
|
||||
// Whether the carousel is vertical or horizontal, interpret y-axis wheel
|
||||
@@ -148,12 +156,15 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
* @param changedProps The changed properties
|
||||
*/
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('_config')) {
|
||||
if (this._config?.size) {
|
||||
this.style.setProperty(
|
||||
'--frigate-card-thumbnail-size',
|
||||
`${this._config.size}px`,
|
||||
);
|
||||
if (changedProps.has('config')) {
|
||||
if (this.config?.size) {
|
||||
this.style.setProperty('--frigate-card-thumbnail-size', `${this.config.size}px`);
|
||||
}
|
||||
const direction = this._getDirection();
|
||||
if (direction) {
|
||||
this.setAttribute('direction', direction);
|
||||
} else {
|
||||
this.removeAttribute('direction');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,17 +174,12 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
* @param changedProperties The properties that were changed in this render.
|
||||
*/
|
||||
updated(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.has('target')) {
|
||||
this._destroyCarousel();
|
||||
}
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has('_selected')) {
|
||||
this.updateComplete.then(() => {
|
||||
if (this._carousel) {
|
||||
if (this._selected !== undefined && this._selected !== null) {
|
||||
this.carouselScrollTo(this._selected);
|
||||
}
|
||||
if (this._selected !== undefined && this._selected !== null) {
|
||||
this._refCarousel.value?.carouselScrollTo(this._selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -209,17 +215,21 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
.target=${parent}
|
||||
.childIndex=${childIndex}
|
||||
.clientID=${cameraConfig?.frigate.client_id}
|
||||
?details=${this._config?.show_details}
|
||||
?show_favorite_control=${this._config?.show_favorite_control}
|
||||
?show_timeline_control=${this._config?.show_timeline_control}
|
||||
?details=${this.config?.show_details}
|
||||
?show_favorite_control=${this.config?.show_favorite_control}
|
||||
?show_timeline_control=${this.config?.show_timeline_control}
|
||||
class="${classMap(classes)}"
|
||||
@click=${(ev) => {
|
||||
if (this._carousel && this._carousel.clickAllowed()) {
|
||||
dispatchFrigateCardEvent<ThumbnailCarouselTap>(this, 'carousel:tap', {
|
||||
slideIndex: slideIndex,
|
||||
target: parent,
|
||||
childIndex: childIndex,
|
||||
});
|
||||
if (this._refCarousel.value?.carouselClickAllowed()) {
|
||||
dispatchFrigateCardEvent<ThumbnailCarouselTap>(
|
||||
this,
|
||||
'thumbnail-carousel:tap',
|
||||
{
|
||||
slideIndex: slideIndex,
|
||||
target: parent,
|
||||
childIndex: childIndex,
|
||||
},
|
||||
);
|
||||
}
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
@@ -227,33 +237,49 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
</frigate-card-thumbnail>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the direction of the thumbnail carousel.
|
||||
* @returns `vertical`, `horizontal` or undefined.
|
||||
*/
|
||||
protected _getDirection(): 'horizontal' | 'vertical' | undefined {
|
||||
if (this.config?.mode === 'left' || this.config?.mode === 'right') {
|
||||
return 'vertical';
|
||||
} else if (this.config?.mode === 'above' || this.config?.mode === 'below') {
|
||||
return 'horizontal';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the element.
|
||||
* @returns A template to display to the user.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
const slides = this._getSlides();
|
||||
if (!slides.length || !this._config || this._config.mode == 'none') {
|
||||
if (!slides.length || !this.config || this.config.mode === 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
return html` <div class="embla">
|
||||
<div class="embla__viewport">
|
||||
<div class="embla__container">${slides}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
return html`<frigate-card-carousel
|
||||
${ref(this._refCarousel)}
|
||||
direction=${ifDefined(this._getDirection())}
|
||||
.carouselOptions=${this._getOptions()}
|
||||
.carouselPlugins=${this._getPlugins()}
|
||||
>
|
||||
${slides}
|
||||
</frigate-card-carousel>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return [super.styles, unsafeCSS(thumbnailCarouselStyle)];
|
||||
return unsafeCSS(thumbnailCarouselStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"frigate-card-thumbnail-carousel": FrigateCardThumbnailCarousel
|
||||
}
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-thumbnail-carousel': FrigateCardThumbnailCarousel;
|
||||
}
|
||||
}
|
||||
|
||||
+104
-140
@@ -11,18 +11,20 @@ import {
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { ref } from 'lit/directives/ref.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import {
|
||||
dispatchFrigateCardErrorEvent,
|
||||
renderProgressIndicator,
|
||||
} from '../components/message.js';
|
||||
import viewerStyle from '../scss/viewer.scss';
|
||||
import type {
|
||||
import viewerCarouselStyle from '../scss/viewer-carousel.scss';
|
||||
import {
|
||||
BrowseMediaNeighbors,
|
||||
BrowseMediaQueryParameters,
|
||||
CameraConfig,
|
||||
ExtendedHomeAssistant,
|
||||
FrigateBrowseMediaSource,
|
||||
frigateCardConfigDefaults,
|
||||
FrigateCardMediaPlayer,
|
||||
MediaShowInfo,
|
||||
TransitionEffect,
|
||||
@@ -39,13 +41,16 @@ import {
|
||||
overrideMultiBrowseMediaQueryParameters,
|
||||
} from '../utils/ha/browse-media.js';
|
||||
import { ResolvedMediaCache, resolveMedia } from '../utils/ha/resolved-media.js';
|
||||
import { createMediaShowInfo } from '../utils/media-info.js';
|
||||
import { View } from '../view.js';
|
||||
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
||||
import { Lazyload } from './embla-plugins/lazyload.js';
|
||||
import { FrigateCardMediaCarousel, IMG_EMPTY } from './media-carousel.js';
|
||||
import {
|
||||
FrigateCardMediaCarousel,
|
||||
IMG_EMPTY,
|
||||
wrapMediaLoadEventForCarousel,
|
||||
wrapMediaShowEventForCarousel,
|
||||
} from './media-carousel.js';
|
||||
import './next-prev-control.js';
|
||||
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
|
||||
import './title-control.js';
|
||||
import '../patches/ha-hls-player';
|
||||
import './surround-thumbnails';
|
||||
@@ -133,7 +138,7 @@ export class FrigateCardViewer extends LitElement {
|
||||
const FRIGATE_CARD_HLS_SELECTOR = 'frigate-card-ha-hls-player';
|
||||
|
||||
@customElement('frigate-card-viewer-carousel')
|
||||
export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
export class FrigateCardViewerCarousel extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: ExtendedHomeAssistant;
|
||||
|
||||
@@ -154,6 +159,8 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
@property({ attribute: false })
|
||||
public resolvedMediaCache?: ResolvedMediaCache;
|
||||
|
||||
protected _refMediaCarousel: Ref<FrigateCardMediaCarousel> = createRef();
|
||||
|
||||
// Mapping of slide # to FrigateBrowseMediaSource child #.
|
||||
// (Folders are not media items that can be rendered).
|
||||
protected _slideToChild: Record<number, number> = {};
|
||||
@@ -187,22 +194,20 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
* @param changedProperties The properties that were changed in this render.
|
||||
*/
|
||||
updated(changedProperties: PropertyValues): void {
|
||||
if (this._carousel && changedProperties.has('viewerConfig')) {
|
||||
this._destroyCarousel();
|
||||
}
|
||||
const frigateCardCarousel = this._refMediaCarousel.value?.frigateCardCarousel();
|
||||
|
||||
if (this._carousel && changedProperties.has('view')) {
|
||||
if (frigateCardCarousel && changedProperties.has('view')) {
|
||||
const oldView = changedProperties.get('view') as View | undefined;
|
||||
if (oldView) {
|
||||
if (oldView.target !== this.view?.target) {
|
||||
// If the media target is different entirely, reset the carousel.
|
||||
this._destroyCarousel();
|
||||
} else if (this.view.childIndex != oldView.childIndex) {
|
||||
if (
|
||||
oldView.target === this.view?.target &&
|
||||
this.view.childIndex != oldView.childIndex
|
||||
) {
|
||||
const slide = this._getSlideForChild(this.view.childIndex);
|
||||
if (slide !== null && slide !== this.carouselSelected()) {
|
||||
if (slide !== null && slide !== frigateCardCarousel.carouselSelected()) {
|
||||
// If the media target is the same as already loaded, but isn't of
|
||||
// the selected slide, scroll to that slide.
|
||||
this.carouselScrollTo(slide);
|
||||
frigateCardCarousel.carouselScrollTo(slide);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,41 +216,6 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
super.updated(changedProperties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the media on the loaded slide.
|
||||
*/
|
||||
protected _autoPlayHandler(): void {
|
||||
if (
|
||||
this.viewerConfig?.auto_play &&
|
||||
['all', 'selected'].includes(this.viewerConfig.auto_play)
|
||||
) {
|
||||
super._autoPlayHandler();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmute the media on the loaded slide.
|
||||
*/
|
||||
protected _autoUnmuteHandler(): void {
|
||||
if (
|
||||
this.viewerConfig?.auto_unmute &&
|
||||
['all', 'selected'].includes(this.viewerConfig.auto_unmute)
|
||||
) {
|
||||
super._autoUnmuteHandler();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the carousel.
|
||||
*/
|
||||
protected _destroyCarousel(): void {
|
||||
super._destroyCarousel();
|
||||
|
||||
// Notes on instance variables:
|
||||
// * this._slideToChild: This is set as part of each render and does not
|
||||
// need to be destroyed here.
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the slide number given a media child number.
|
||||
* @param childIndex The child index (relative to `view.target`)
|
||||
@@ -265,8 +235,11 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
* Get the transition effect to use.
|
||||
* @returns An TransitionEffect object.
|
||||
*/
|
||||
protected _getTransitionEffect(): TransitionEffect | undefined {
|
||||
return this.viewerConfig?.transition_effect;
|
||||
protected _getTransitionEffect(): TransitionEffect {
|
||||
return (
|
||||
this.viewerConfig?.transition_effect ??
|
||||
frigateCardConfigDefaults.media_viewer.transition_effect
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,16 +259,16 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
* @param slide An optional slide.
|
||||
* @returns The FrigateCardMediaPlayer or null if not found.
|
||||
*/
|
||||
protected _getPlayer(slide?: HTMLElement): FrigateCardMediaPlayer | null {
|
||||
if (this._carousel) {
|
||||
if (!slide) {
|
||||
slide = this._carousel.slideNodes()[this._carousel.selectedScrollSnap()];
|
||||
}
|
||||
return slide?.querySelector(
|
||||
FRIGATE_CARD_HLS_SELECTOR,
|
||||
) as FrigateCardMediaPlayer | null;
|
||||
protected _getPlayer(slide?: HTMLElement | null): FrigateCardMediaPlayer | null {
|
||||
if (!slide) {
|
||||
slide = this._refMediaCarousel.value
|
||||
?.frigateCardCarousel()
|
||||
?.carouselSelectedElement();
|
||||
}
|
||||
return null;
|
||||
|
||||
return (
|
||||
(slide?.querySelector(FRIGATE_CARD_HLS_SELECTOR) as FrigateCardMediaPlayer) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,7 +277,6 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
*/
|
||||
protected _getPlugins(): EmblaPluginType[] {
|
||||
return [
|
||||
...super._getPlugins(),
|
||||
// Only enable wheel plugin if there is more than one media item.
|
||||
...(this.view &&
|
||||
this.view.target &&
|
||||
@@ -480,15 +452,17 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
/**
|
||||
* Handle the user selecting a new slide in the carousel.
|
||||
*/
|
||||
protected _selectSlideSetViewHandler(): void {
|
||||
if (!this._carousel || !this.view) {
|
||||
protected _setViewHandler(): void {
|
||||
if (!this._refMediaCarousel.value || !this.view) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the childIndex in the view.
|
||||
const slidesInView = this._carousel.slidesInView(true);
|
||||
if (slidesInView.length) {
|
||||
const childIndex = this._slideToChild[slidesInView[0]];
|
||||
const selected = this._refMediaCarousel.value
|
||||
.frigateCardCarousel()
|
||||
?.carouselSelected();
|
||||
if (selected !== undefined) {
|
||||
const childIndex = this._slideToChild[selected];
|
||||
if (childIndex !== undefined) {
|
||||
this.view
|
||||
.evolve({
|
||||
@@ -556,31 +530,6 @@ 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 and an index keyed by slide
|
||||
@@ -647,59 +596,59 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
protected _render(): TemplateResult | void {
|
||||
const [slides, slideToChild] = this._getSlides();
|
||||
this._slideToChild = slideToChild;
|
||||
if (!slides.length) {
|
||||
if (!slides.length || !this.view?.media) {
|
||||
return;
|
||||
}
|
||||
|
||||
const neighbors = this._getMediaNeighbors();
|
||||
const [prev, next] = [neighbors?.previous, neighbors?.next];
|
||||
|
||||
return html`<div class="embla">
|
||||
<frigate-card-next-previous-control
|
||||
${ref(this._previousControlRef)}
|
||||
.direction=${'previous'}
|
||||
.controlConfig=${this.viewerConfig?.controls.next_previous}
|
||||
.thumbnail=${prev && prev.thumbnail ? prev.thumbnail : undefined}
|
||||
.label=${prev ? prev.title : ''}
|
||||
?disabled=${!prev}
|
||||
@click=${(ev) => {
|
||||
this._nextPreviousHandler('previous');
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
></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=${this.viewerConfig?.controls.next_previous}
|
||||
.thumbnail=${next && next.thumbnail ? next.thumbnail : undefined}
|
||||
.label=${next ? next.title : ''}
|
||||
?disabled=${!next}
|
||||
@click=${(ev) => {
|
||||
this._nextPreviousHandler('next');
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
></frigate-card-next-previous-control>
|
||||
</div>
|
||||
${this.view?.media
|
||||
? html` <frigate-card-title-control
|
||||
${ref(this._titleControlRef)}
|
||||
.config=${this.viewerConfig?.controls.title}
|
||||
.text="${this.view.media.title}"
|
||||
.fitInto=${this as HTMLElement}
|
||||
>
|
||||
</frigate-card-title-control>`
|
||||
: ``} `;
|
||||
return html` <frigate-card-media-carousel
|
||||
${ref(this._refMediaCarousel)}
|
||||
.carouselOptions=${this._getOptions()}
|
||||
.carouselPlugins=${this._getPlugins()}
|
||||
.autoPlayCondition=${this.viewerConfig?.auto_play}
|
||||
.autoPauseCondition=${this.viewerConfig?.auto_pause}
|
||||
.autoMuteCondition=${this.viewerConfig?.auto_mute}
|
||||
.autoUnmuteCondition=${this.viewerConfig?.auto_unmute}
|
||||
.label="${this.view.media.title}"
|
||||
.titlePopupConfig=${this.viewerConfig?.controls.title}
|
||||
transitionEffect=${this._getTransitionEffect()}
|
||||
@frigate-card:carousel:select=${this._setViewHandler.bind(this)}
|
||||
@frigate-card:media-show=${this._recordingSeekHandler.bind(this)}
|
||||
>
|
||||
<frigate-card-next-previous-control
|
||||
slot="previous"
|
||||
.direction=${'previous'}
|
||||
.controlConfig=${this.viewerConfig?.controls.next_previous}
|
||||
.thumbnail=${prev && prev.thumbnail ? prev.thumbnail : undefined}
|
||||
.label=${prev ? prev.title : ''}
|
||||
?disabled=${!prev}
|
||||
@click=${(ev) => {
|
||||
this._refMediaCarousel.value?.frigateCardCarousel()?.carouselScrollPrevious();
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
></frigate-card-next-previous-control>
|
||||
${slides}
|
||||
<frigate-card-next-previous-control
|
||||
slot="next"
|
||||
.direction=${'next'}
|
||||
.controlConfig=${this.viewerConfig?.controls.next_previous}
|
||||
.thumbnail=${next && next.thumbnail ? next.thumbnail : undefined}
|
||||
.label=${next ? next.title : ''}
|
||||
?disabled=${!next}
|
||||
@click=${(ev) => {
|
||||
this._refMediaCarousel.value?.frigateCardCarousel()?.carouselScrollNext();
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
></frigate-card-next-previous-control>
|
||||
</frigate-card-media-carousel>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a media show event when a slide is selected.
|
||||
*/
|
||||
protected _selectSlideMediaShowHandler(): void {
|
||||
super._selectSlideMediaShowHandler();
|
||||
|
||||
protected _recordingSeekHandler(): void {
|
||||
// If this is a recording and play is desired to be started from a
|
||||
// particular point, seek to that point. Use the media off the slide itself
|
||||
// -- when the slide is changed, the media show event may be dispatched
|
||||
@@ -751,8 +700,9 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
)}
|
||||
.media=${mediaToRender}
|
||||
.hass=${this.hass}
|
||||
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) =>
|
||||
this._mediaShowEventHandler(slideIndex, e)}
|
||||
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) => {
|
||||
wrapMediaShowEventForCarousel(slideIndex, e);
|
||||
}}
|
||||
>
|
||||
</frigate-card-ha-hls-player>`
|
||||
: html`<img
|
||||
@@ -762,7 +712,11 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
)}
|
||||
title="${mediaToRender.title}"
|
||||
@click=${() => {
|
||||
if (this._carousel?.clickAllowed()) {
|
||||
if (
|
||||
this._refMediaCarousel.value
|
||||
?.frigateCardCarousel()
|
||||
?.carouselClickAllowed()
|
||||
) {
|
||||
this._findRelatedClipView(mediaToRender).then((view) => {
|
||||
if (view) {
|
||||
view.dispatchChangeEvent(this);
|
||||
@@ -771,6 +725,9 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
}
|
||||
}}
|
||||
@load="${(e: Event) => {
|
||||
const lazyloadPlugin = this._refMediaCarousel.value
|
||||
?.frigateCardCarousel()
|
||||
?.getCarouselPlugins()?.lazyload;
|
||||
if (
|
||||
// This handler will be called on the empty image (including
|
||||
// an updated empty image that is the same dimensions large as
|
||||
@@ -778,15 +735,22 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
// images in media-carousel.ts). Here we need to only call the
|
||||
// media load handler on a 'real' load.
|
||||
!lazyLoad ||
|
||||
this._getLazyLoadPlugin()?.hasLazyloaded(slideIndex)
|
||||
lazyloadPlugin?.hasLazyloaded(slideIndex)
|
||||
) {
|
||||
this._mediaLoadedHandler(slideIndex, createMediaShowInfo(e));
|
||||
wrapMediaLoadEventForCarousel(slideIndex, e);
|
||||
}
|
||||
}}"
|
||||
/>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(viewerCarouselStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
Reference in New Issue
Block a user