diff --git a/src/components/carousel.ts b/src/components/carousel.ts index b7007314..955efcac 100644 --- a/src/components/carousel.ts +++ b/src/components/carousel.ts @@ -31,6 +31,14 @@ export class FrigateCardCarousel extends LitElement { this._carousel?.scrollTo(index); } + /** + * Get the selected slide. + * @returns The slide index or undefined if the carousel is not loaded. + */ + carouselSelected(): number | undefined { + return this._carousel?.selectedScrollSnap(); + } + /** * The updated lifecycle callback for this element. * @param changedProperties The properties that were changed in this render. @@ -61,16 +69,14 @@ export class FrigateCardCarousel extends LitElement { '.embla__viewport', ) as HTMLElement; - if (carouselNode && !this._carousel) { + if (!this._carousel && carouselNode) { this._carousel = EmblaCarousel(carouselNode, this._options); this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init')); - this._carousel.on('resize', () => - dispatchFrigateCardEvent(this, 'carousel:resize'), - ); this._carousel.on('select', () => { - if (this._carousel) { + const selected = this.carouselSelected(); + if (selected !== undefined) { dispatchFrigateCardEvent(this, 'carousel:select', { - index: this._carousel.selectedScrollSnap(), + index: selected, }); } }); diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts index 0e5bc73e..3438ab39 100644 --- a/src/components/thumbnail-carousel.ts +++ b/src/components/thumbnail-carousel.ts @@ -14,7 +14,7 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { @property({ attribute: false }) protected target?: BrowseMediaSource; - protected _tapSelected? = 0; + protected _tapSelected?; constructor() { super(); diff --git a/src/components/viewer.ts b/src/components/viewer.ts index 76c4caa2..0797e734 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -4,10 +4,9 @@ import { TemplateResult, html, unsafeCSS, - PropertyValues, } from 'lit'; import { BrowseMediaUtil } from '../browse-media-util.js'; -import EmblaCarousel, { EmblaCarouselType } from 'embla-carousel'; +import { EmblaCarouselType } from 'embla-carousel'; import { HomeAssistant } from 'custom-card-helpers'; import { createRef, Ref, ref } from 'lit/directives/ref.js'; import { customElement, property } from 'lit/decorators.js'; @@ -22,7 +21,7 @@ import type { MediaShowInfo, ViewerConfig, } from '../types.js'; -import { CarouselTap } from './carousel.js'; +import { CarouselTap, FrigateCardCarousel } from './carousel.js'; import { FrigateCardThumbnailCarousel } from './thumbnail-carousel.js'; import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js'; import { View } from '../view.js'; @@ -44,6 +43,7 @@ import './thumbnail-carousel.js'; import viewerStyle from '../scss/viewer.scss'; import viewerCoreStyle from '../scss/viewer-core.scss'; +import mediaCarouselStyle from '../scss/media-carousel.scss'; 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`; @@ -171,12 +171,68 @@ export class FrigateCardViewerCore extends LitElement { @property({ attribute: false }) protected resolvedMediaCache?: ResolvedMediaCache; - // Media carousel object. - protected _carousel?: EmblaCarouselType; - protected _loadedCarousel = false; - + protected _mediaCarouselRef: Ref = createRef(); protected _thumbnailCarouselRef: Ref = createRef(); + protected _syncThumbnailCarousel(): void { + const mediaSelected = this._mediaCarouselRef.value?.carouselSelected(); + if (mediaSelected !== undefined) { + this._thumbnailCarouselRef.value?.carouselScrollTo(mediaSelected); + } + } + + protected render(): TemplateResult | void { + if (!this.view) { + return html``; + } + return html` + + + ) => { + this._mediaCarouselRef.value?.carouselScrollTo(ev.detail.index); + }} + @frigate-card:carousel:init=${this._syncThumbnailCarousel.bind(this)} + > + `; + } + + /** + * Get element styles. + */ + static get styles(): CSSResultGroup { + return unsafeCSS(viewerCoreStyle); + } +} + + +@customElement('frigate-card-media-carousel') +export class FrigateCardMediaCarousel extends FrigateCardCarousel { + @property({ attribute: false }) + protected hass?: HomeAssistant & ExtendedHomeAssistant; + + @property({ attribute: false }) + protected view?: View; + + @property({ attribute: false }) + protected viewerConfig?: ViewerConfig; + + @property({ attribute: false }) + protected browseMediaQueryParameters?: BrowseMediaQueryParameters; + + @property({ attribute: false }) + protected resolvedMediaCache?: ResolvedMediaCache; + // Mapping of slide # to BrowseMediaSource child #. // (Folders are not media items that can be rendered). protected _slideToChild: Record = {}; @@ -187,64 +243,42 @@ export class FrigateCardViewerCore extends LitElement { // Whether or not a given slide has been successfully lazily loaded. protected _slideHasBeenLazyLoaded: Record = {}; - /** - * The updated lifecycle callback for this element. - * @param changedProperties The properties that were changed in this render. - */ - updated(changedProperties: PropertyValues): void { - super.updated(changedProperties); - - if (!this._loadedCarousel) { - this.updateComplete.then(() => { - this._loadCarousel(); - }); - } - } - /** * Load the carousel with "slides" (clips or snapshots). */ protected _loadCarousel(): void { - const carouselNode = this.renderRoot.querySelector( - '.embla__viewport', - ) as HTMLElement; - - if (carouselNode && this.viewerConfig) { - this._loadedCarousel = true; - - // Start the carousel on the selected child number. - const startIndex = Number( - Object.keys(this._slideToChild).find( - (key) => this._slideToChild[key] === this.view?.childIndex, - ), - ); - - this._carousel = EmblaCarousel(carouselNode, { - startIndex: isNaN(startIndex) ? undefined : startIndex, - draggable: this.viewerConfig.draggable, - }); - // Update views and dispatch media-show events based on slide selections. - this._carousel.on('select', this._selectSlideSetViewHandler.bind(this)); - this._carousel.on('select', this._selectSlideMediaShowHandler.bind(this)); - - // Lazily load media that is displayed. These handlers are registered - // regardless of the value of this.lazyLoad to allow that value to change - // after the carousel has been initialized. - this._carousel.on('init', this._lazyLoadMediaHandler.bind(this)); - this._carousel.on('select', this._lazyLoadMediaHandler.bind(this)); - this._carousel.on('resize', this._lazyLoadMediaHandler.bind(this)); - - this._carousel.on('select', this._syncThumbnailCarousel.bind(this)); - } - } - - protected _syncThumbnailCarousel(): void { - if (!this._carousel) { + if (this._carousel || !this.viewerConfig) { return; } + + // Start the carousel on the selected child number. + const startIndex = Number( + Object.keys(this._slideToChild).find( + (key) => this._slideToChild[key] === this.view?.childIndex, + ), + ); - this._thumbnailCarouselRef.value?.carouselScrollTo( - this._carousel.selectedScrollSnap()); + this._options = { + startIndex: isNaN(startIndex) ? undefined : startIndex, + draggable: this.viewerConfig.draggable, + } + + super._loadCarousel(); + + // Necessary because typescript local type narrowing is not paying attention + // to the side-effect of the call to super._loadCarousel(). + const carousel = this._carousel as EmblaCarouselType | undefined; + + // Update views and dispatch media-show events based on slide selections. + carousel?.on('select', this._selectSlideSetViewHandler.bind(this)); + carousel?.on('select', this._selectSlideMediaShowHandler.bind(this)); + + // Lazily load media that is displayed. These handlers are registered + // regardless of the value of this.lazyLoad to allow that value to change + // after the carousel has been initialized. + carousel?.on('init', this._lazyLoadMediaHandler.bind(this)); + carousel?.on('select', this._lazyLoadMediaHandler.bind(this)); + carousel?.on('resize', this._lazyLoadMediaHandler.bind(this)); } /** @@ -532,15 +566,7 @@ export class FrigateCardViewerCore extends LitElement { }} >` : ``} - - ) => this._carousel?.scrollTo(ev.detail.index)} - @frigate-card:carousel:init=${this._syncThumbnailCarousel.bind(this)} - > - `; + `; } /** @@ -722,6 +748,6 @@ export class FrigateCardViewerCore extends LitElement { * Get element styles. */ static get styles(): CSSResultGroup { - return unsafeCSS(viewerCoreStyle); + return [super.styles, unsafeCSS(mediaCarouselStyle)]; } } diff --git a/src/scss/media-carousel.scss b/src/scss/media-carousel.scss new file mode 100644 index 00000000..39c877d0 --- /dev/null +++ b/src/scss/media-carousel.scss @@ -0,0 +1,13 @@ +:host { + --video-max-height: none; +} + +.embla__slide { + flex: 0 0 100%; +} + +frigate-card-ha-hls-player { + height: 100%; + width: 100%; + transform-style: preserve-3d; +} \ No newline at end of file diff --git a/src/scss/thumbnail-carousel.scss b/src/scss/thumbnail-carousel.scss index aca8fe51..6dd52e2d 100644 --- a/src/scss/thumbnail-carousel.scss +++ b/src/scss/thumbnail-carousel.scss @@ -1,3 +1,7 @@ +:host { + --frigate-card-viewer-thumbnail-size: 100px; +} + .embla__slide { flex: 0 0 var(--frigate-card-viewer-thumbnail-size); opacity: 0.4; diff --git a/src/scss/viewer-core.scss b/src/scss/viewer-core.scss index 83bc822b..b3539631 100644 --- a/src/scss/viewer-core.scss +++ b/src/scss/viewer-core.scss @@ -4,75 +4,14 @@ height: 100%; width: 100%; - - --video-max-height: none; - --frigate-card-viewer-thumbnail-size: 100px; } -img,video { - width: 100%; - height: 100%; - display: block; -} - -.embla { - position: relative; - margin-left: auto; - margin-right: auto; -} - -// Master containers, thumbnails are a fixed size and the main viewer panel -// should grow/shrink accordingly. -.embla { +frigate-card-media-carousel { flex: 1; min-height: 0; } + frigate-card-thumbnail-carousel { flex: 0 0 var(--frigate-card-viewer-thumbnail-size); margin-top: 5px; -} - -.embla__container { - display: flex; - width: 100%; - height: 100%; - - user-select: none; - -webkit-touch-callout: none; - -khtml-user-select: none; - -webkit-tap-highlight-color: transparent; -} - -.embla__viewport { - width: 100%; - height: 100%; - overflow: hidden; -} -.embla__viewport.is-draggable { - cursor: move; - cursor: grab; -} -.embla__viewport.is-dragging { - cursor: grabbing; -} - -.embla__slide { - position: relative; - height: 100%; - margin-right: 5px; - overflow: hidden; -} -.embla__slide { - flex: 0 0 100%; -} -.embla__slide img,video { - // Letterbox media. has similar added directly in - // its element. - object-fit: contain; -} - -frigate-card-ha-hls-player { - height: 100%; - width: 100%; - transform-style: preserve-3d; } \ No newline at end of file