= 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`
- ) =>
- this._mediaShowEventHandler(slideIndex, e)}
- >
-
-
`;
+ return html`
+
+ ) => {
+ wrapMediaShowEventForCarousel(slideIndex, e)
+ }}
+ >
+
+
+ `;
}
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`
-
+
{
- this._nextPreviousHandler('previous');
+ this._refMediaCarousel.value
+ ?.frigateCardCarousel()
+ ?.carouselScrollPrevious();
stopEventFromActivatingCardWideActions(ev);
}}
>
-
+ ${slides}
{
- this._nextPreviousHandler('next');
+ this._refMediaCarousel.value?.frigateCardCarousel()?.carouselScrollNext();
stopEventFromActivatingCardWideActions(ev);
}}
>
-
-
-
+
`;
}
+
+ /**
+ * 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`
`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(
+ 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,
+) => {
+ 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 = {};
protected _nextControlRef: Ref = createRef();
@@ -28,12 +125,19 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
protected _titleControlRef: Ref = 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 = 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,
- ): void {
+ protected _storeMediaShowInfo(event: CustomEvent): 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`
+
+
+
+
+ ${this.label && this.titlePopupConfig
+ ? html`
+ `
+ : ``}`;
+ }
+
/**
* 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;
+ }
}
diff --git a/src/components/surround-thumbnails.ts b/src/components/surround-thumbnails.ts
index 2960fc87..4d701f07 100644
--- a/src/components/surround-thumbnails.ts
+++ b/src/components/surround-thumbnails.ts
@@ -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) => {
+ @frigate-card:thumbnail-carousel:tap=${(ev: CustomEvent) => {
// 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
diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts
index 5fa1509a..785bdab1 100644
--- a/src/components/thumbnail-carousel.ts
+++ b/src/components/thumbnail-carousel.ts
@@ -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;
+ protected _refCarousel: Ref = 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(this, 'carousel:tap', {
- slideIndex: slideIndex,
- target: parent,
- childIndex: childIndex,
- });
+ if (this._refCarousel.value?.carouselClickAllowed()) {
+ dispatchFrigateCardEvent(
+ this,
+ 'thumbnail-carousel:tap',
+ {
+ slideIndex: slideIndex,
+ target: parent,
+ childIndex: childIndex,
+ },
+ );
}
stopEventFromActivatingCardWideActions(ev);
}}
@@ -227,33 +237,49 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
`;
}
+ /**
+ * 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` `;
+ return html`
+ ${slides}
+ `;
}
/**
* 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;
+ }
}
diff --git a/src/components/viewer.ts b/src/components/viewer.ts
index 9a560995..1f96beee 100644
--- a/src/components/viewer.ts
+++ b/src/components/viewer.ts
@@ -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 = createRef();
+
// Mapping of slide # to FrigateBrowseMediaSource child #.
// (Folders are not media items that can be rendered).
protected _slideToChild: Record = {};
@@ -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`
-
{
- this._nextPreviousHandler('previous');
- stopEventFromActivatingCardWideActions(ev);
- }}
- >
-
-
{
- this._nextPreviousHandler('next');
- stopEventFromActivatingCardWideActions(ev);
- }}
- >
-
- ${this.view?.media
- ? html`
- `
- : ``} `;
+ return html`
+ {
+ this._refMediaCarousel.value?.frigateCardCarousel()?.carouselScrollPrevious();
+ stopEventFromActivatingCardWideActions(ev);
+ }}
+ >
+ ${slides}
+ {
+ this._refMediaCarousel.value?.frigateCardCarousel()?.carouselScrollNext();
+ stopEventFromActivatingCardWideActions(ev);
+ }}
+ >
+ `;
}
/**
* 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) =>
- this._mediaShowEventHandler(slideIndex, e)}
+ @frigate-card:media-show=${(e: CustomEvent) => {
+ wrapMediaShowEventForCarousel(slideIndex, e);
+ }}
>
`
: html`
{
- 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);
}
}}"
/>`}
`;
}
+
+ /**
+ * Get element styles.
+ */
+ static get styles(): CSSResultGroup {
+ return unsafeCSS(viewerCarouselStyle);
+ }
}
declare global {
diff --git a/src/scss/carousel.scss b/src/scss/carousel.scss
index d72a6def..167146a8 100644
--- a/src/scss/carousel.scss
+++ b/src/scss/carousel.scss
@@ -4,16 +4,9 @@
width: 100%;
}
-img,video {
- width: 100%;
- height: 100%;
- display: block;
-}
-
.embla {
width: 100%;
height: 100%;
- position: relative;
margin-left: auto;
margin-right: auto;
}
@@ -28,10 +21,10 @@ img,video {
-khtml-user-select: none;
-webkit-tap-highlight-color: transparent;
}
-:host([direction=vertical]) .embla__container {
+:host([direction='vertical']) .embla__container {
flex-direction: column;
}
-:host([direction=horizontal]) .embla__container {
+:host([direction='horizontal']) .embla__container {
flex-direction: row;
}
@@ -53,18 +46,10 @@ img,video {
cursor: grabbing;
}
-.embla__slide {
- position: relative;
- overflow: visible;
-}
-:host([direction=vertical]) .embla__slide {
+:host([direction='vertical']) ::slotted(.embla__slide) {
margin-bottom: 5px;
}
-:host([direction=horizontal]) .embla__slide {
+
+:host([direction='horizontal']) ::slotted(.embla__slide) {
margin-right: 5px;
}
-.embla__slide img,video {
- // Letterbox media. has similar added directly in
- // its element.
- object-fit: contain;
-}
\ No newline at end of file
diff --git a/src/scss/live-carousel.scss b/src/scss/live-carousel.scss
new file mode 100644
index 00000000..aab21ed3
--- /dev/null
+++ b/src/scss/live-carousel.scss
@@ -0,0 +1,4 @@
+.embla__slide {
+ height: 100%;
+ flex: 0 0 100%;
+}
diff --git a/src/scss/media-carousel.scss b/src/scss/media-carousel.scss
index d0d0142d..45964ad6 100644
--- a/src/scss/media-carousel.scss
+++ b/src/scss/media-carousel.scss
@@ -1,8 +1,9 @@
:host {
- --video-max-height: none;
-}
-
-.embla__slide {
- flex: 0 0 100%;
+ display: block;
+ width: 100%;
height: 100%;
-}
\ No newline at end of file
+ --video-max-height: none;
+
+ // Keep the controls relative to the media carousel itself.
+ position: relative;
+}
diff --git a/src/scss/next-previous-control.scss b/src/scss/next-previous-control.scss
index d72d61bd..2d9e97b0 100644
--- a/src/scss/next-previous-control.scss
+++ b/src/scss/next-previous-control.scss
@@ -22,7 +22,7 @@
}
.controls.icons {
- top: calc(50% - (40px / 2));
+ top: calc(50% - (var(--frigate-card-next-prev-size) / 2));
}
.controls.thumbnails {
diff --git a/src/scss/thumbnail-carousel.scss b/src/scss/thumbnail-carousel.scss
index daa8c5d9..c0774cd5 100644
--- a/src/scss/thumbnail-carousel.scss
+++ b/src/scss/thumbnail-carousel.scss
@@ -1,6 +1,10 @@
@use 'const.scss';
:host {
+ display: block;
+ width: 100%;
+ height: 100%;
+
--frigate-card-carousel-thumbnail-opacity: 1;
}
diff --git a/src/scss/thumbnail-feature-event.scss b/src/scss/thumbnail-feature-event.scss
index 1ca3328e..88a9bbfb 100644
--- a/src/scss/thumbnail-feature-event.scss
+++ b/src/scss/thumbnail-feature-event.scss
@@ -3,6 +3,10 @@
overflow: hidden;
}
+img {
+ display: block;
+}
+
img,
ha-icon {
border-radius: var(--ha-card-border-radius, 4px);
diff --git a/src/scss/viewer-carousel.scss b/src/scss/viewer-carousel.scss
new file mode 100644
index 00000000..5e7f7390
--- /dev/null
+++ b/src/scss/viewer-carousel.scss
@@ -0,0 +1,14 @@
+.embla__slide {
+ height: 100%;
+ flex: 0 0 100%;
+}
+
+.embla__slide img {
+ display: block;
+ width: 100%;
+ height: 100%;
+
+ // Letterbox media. has similar added directly in
+ // its element.
+ object-fit: contain;
+}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 8da90b3b..a633cb76 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1155,17 +1155,17 @@ electron-to-chromium@^1.4.147:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.161.tgz#49cb5b35385bfee6cc439d0a04fbba7a7a7f08a1"
integrity sha512-sTjBRhqh6wFodzZtc5Iu8/R95OkwaPNn7tj/TaDU5nu/5EFiQDtADGAXdR4tJcTEHlYfJpHqigzJqHvPgehP8A==
-embla-carousel-wheel-gestures@^2.1.1:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/embla-carousel-wheel-gestures/-/embla-carousel-wheel-gestures-2.2.0.tgz#04ee1cfafe0667a5b96d16341642b3124fe8894e"
- integrity sha512-IoRGblg8QWrIgZEW0NbDcIl2fO++BLFf6197k2JNair3pfbyiMYtva6rROgeRiI0sIj2kFwlSEgudTy5f8TzNQ==
+embla-carousel-wheel-gestures@^3.0.0-rc01:
+ version "3.0.0-rc01"
+ resolved "https://registry.yarnpkg.com/embla-carousel-wheel-gestures/-/embla-carousel-wheel-gestures-3.0.0-rc01.tgz#70f88d6ee755817270ca26514d06b7c08c12977d"
+ integrity sha512-h6E1/AwGKEwro8pey6KeOnt/UMvSaCwJKxaA+sz4OERPLmVL06oejVJ2kMjL06y9WaxZ8TqKWR7m0HlbsI9F5A==
dependencies:
wheel-gestures "^2.2.5"
-embla-carousel@^7.0.0-rc01:
- version "7.0.0-rc01"
- resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-7.0.0-rc01.tgz#7c9adfd7302b85c2de9354b7ef6343f16a696eb7"
- integrity sha512-IBTSKcPw7u9K0zoLvsnWYsijKsI0msFqzNp6ASIthTXiMZNJNGQt8k0ax7KqUVinDZeNM07WPWqYsjrvUM/Epw==
+embla-carousel@^7.0.0-rc04:
+ version "7.0.0-rc04"
+ resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-7.0.0-rc04.tgz#bd9c15da7740660b46232fba720b0c38043b667f"
+ integrity sha512-vhzwCEdEqwS5c6jlfPHy/X1uUkwf9AvMma8KWNbmTgB3NUNN9hoqiyTdrpraMtPU5MzlxpZipln74ZwDK4v63g==
emojis-list@^3.0.0:
version "3.0.0"