Convert viewer to inherit from media carousel.

This commit is contained in:
Dermot Duffy
2021-11-24 13:46:23 -08:00
parent 48f288e0fd
commit 2e9be783a6
6 changed files with 126 additions and 138 deletions
+12 -6
View File
@@ -31,6 +31,14 @@ export class FrigateCardCarousel extends LitElement {
this._carousel?.scrollTo(index); 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. * The updated lifecycle callback for this element.
* @param changedProperties The properties that were changed in this render. * @param changedProperties The properties that were changed in this render.
@@ -61,16 +69,14 @@ export class FrigateCardCarousel extends LitElement {
'.embla__viewport', '.embla__viewport',
) as HTMLElement; ) as HTMLElement;
if (carouselNode && !this._carousel) { if (!this._carousel && carouselNode) {
this._carousel = EmblaCarousel(carouselNode, this._options); this._carousel = EmblaCarousel(carouselNode, this._options);
this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init')); this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init'));
this._carousel.on('resize', () =>
dispatchFrigateCardEvent(this, 'carousel:resize'),
);
this._carousel.on('select', () => { this._carousel.on('select', () => {
if (this._carousel) { const selected = this.carouselSelected();
if (selected !== undefined) {
dispatchFrigateCardEvent<CarouselSelect>(this, 'carousel:select', { dispatchFrigateCardEvent<CarouselSelect>(this, 'carousel:select', {
index: this._carousel.selectedScrollSnap(), index: selected,
}); });
} }
}); });
+1 -1
View File
@@ -14,7 +14,7 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
@property({ attribute: false }) @property({ attribute: false })
protected target?: BrowseMediaSource; protected target?: BrowseMediaSource;
protected _tapSelected? = 0; protected _tapSelected?;
constructor() { constructor() {
super(); super();
+94 -68
View File
@@ -4,10 +4,9 @@ import {
TemplateResult, TemplateResult,
html, html,
unsafeCSS, unsafeCSS,
PropertyValues,
} from 'lit'; } from 'lit';
import { BrowseMediaUtil } from '../browse-media-util.js'; 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 { HomeAssistant } from 'custom-card-helpers';
import { createRef, Ref, ref } from 'lit/directives/ref.js'; import { createRef, Ref, ref } from 'lit/directives/ref.js';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
@@ -22,7 +21,7 @@ import type {
MediaShowInfo, MediaShowInfo,
ViewerConfig, ViewerConfig,
} from '../types.js'; } from '../types.js';
import { CarouselTap } from './carousel.js'; import { CarouselTap, FrigateCardCarousel } from './carousel.js';
import { FrigateCardThumbnailCarousel } from './thumbnail-carousel.js'; import { FrigateCardThumbnailCarousel } from './thumbnail-carousel.js';
import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js'; import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js';
import { View } from '../view.js'; import { View } from '../view.js';
@@ -44,6 +43,7 @@ import './thumbnail-carousel.js';
import viewerStyle from '../scss/viewer.scss'; import viewerStyle from '../scss/viewer.scss';
import viewerCoreStyle from '../scss/viewer-core.scss'; import viewerCoreStyle from '../scss/viewer-core.scss';
import mediaCarouselStyle from '../scss/media-carousel.scss';
const getEmptyImageSrc = (width: number, height: number) => 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`; `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 }) @property({ attribute: false })
protected resolvedMediaCache?: ResolvedMediaCache; protected resolvedMediaCache?: ResolvedMediaCache;
// Media carousel object. protected _mediaCarouselRef: Ref<FrigateCardMediaCarousel> = createRef();
protected _carousel?: EmblaCarouselType;
protected _loadedCarousel = false;
protected _thumbnailCarouselRef: Ref<FrigateCardThumbnailCarousel> = createRef(); protected _thumbnailCarouselRef: Ref<FrigateCardThumbnailCarousel> = 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`
<frigate-card-media-carousel
${ref(this._mediaCarouselRef)}
.hass=${this.hass}
.view=${this.view}
.viewerConfig=${this.viewerConfig}
.browseMediaQueryParameters=${this.browseMediaQueryParameters}
.resolvedMediaCache=${this.resolvedMediaCache}
@frigate-card:carousel:select=${this._syncThumbnailCarousel.bind(this)}
>
</frigate-card-media-carousel>
<frigate-card-thumbnail-carousel
${ref(this._thumbnailCarouselRef)}
.target=${this.view.target}
@frigate-card:carousel:tap=${(ev: CustomEvent<CarouselTap>) => {
this._mediaCarouselRef.value?.carouselScrollTo(ev.detail.index);
}}
@frigate-card:carousel:init=${this._syncThumbnailCarousel.bind(this)}
>
</frigate-card-thumbnail-carousel>`;
}
/**
* 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 #. // Mapping of slide # to BrowseMediaSource child #.
// (Folders are not media items that can be rendered). // (Folders are not media items that can be rendered).
protected _slideToChild: Record<number, number> = {}; protected _slideToChild: Record<number, number> = {};
@@ -187,64 +243,42 @@ export class FrigateCardViewerCore extends LitElement {
// Whether or not a given slide has been successfully lazily loaded. // Whether or not a given slide has been successfully lazily loaded.
protected _slideHasBeenLazyLoaded: Record<number, boolean> = {}; protected _slideHasBeenLazyLoaded: Record<number, boolean> = {};
/**
* 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). * Load the carousel with "slides" (clips or snapshots).
*/ */
protected _loadCarousel(): void { protected _loadCarousel(): void {
const carouselNode = this.renderRoot.querySelector( if (this._carousel || !this.viewerConfig) {
'.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) {
return; return;
} }
this._thumbnailCarouselRef.value?.carouselScrollTo( // Start the carousel on the selected child number.
this._carousel.selectedScrollSnap()); const startIndex = Number(
Object.keys(this._slideToChild).find(
(key) => this._slideToChild[key] === this.view?.childIndex,
),
);
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 {
}} }}
></frigate-card-next-previous-control>` ></frigate-card-next-previous-control>`
: ``} : ``}
</div> </div>`;
<frigate-card-thumbnail-carousel
${ref(this._thumbnailCarouselRef)}
.target=${this.view.target}
.selected=${this._carousel?.selectedScrollSnap()}
@frigate-card:carousel:tap=${(ev: CustomEvent<CarouselTap>) => this._carousel?.scrollTo(ev.detail.index)}
@frigate-card:carousel:init=${this._syncThumbnailCarousel.bind(this)}
>
</frigate-card-thumbnail-carousel>`;
} }
/** /**
@@ -722,6 +748,6 @@ export class FrigateCardViewerCore extends LitElement {
* Get element styles. * Get element styles.
*/ */
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return unsafeCSS(viewerCoreStyle); return [super.styles, unsafeCSS(mediaCarouselStyle)];
} }
} }
+13
View File
@@ -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;
}
+4
View File
@@ -1,3 +1,7 @@
:host {
--frigate-card-viewer-thumbnail-size: 100px;
}
.embla__slide { .embla__slide {
flex: 0 0 var(--frigate-card-viewer-thumbnail-size); flex: 0 0 var(--frigate-card-viewer-thumbnail-size);
opacity: 0.4; opacity: 0.4;
+2 -63
View File
@@ -4,75 +4,14 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
--video-max-height: none;
--frigate-card-viewer-thumbnail-size: 100px;
} }
img,video { frigate-card-media-carousel {
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 {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
} }
frigate-card-thumbnail-carousel { frigate-card-thumbnail-carousel {
flex: 0 0 var(--frigate-card-viewer-thumbnail-size); flex: 0 0 var(--frigate-card-viewer-thumbnail-size);
margin-top: 5px; 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. <frigate-card-ha-hls-player> has similar added directly in
// its element.
object-fit: contain;
}
frigate-card-ha-hls-player {
height: 100%;
width: 100%;
transform-style: preserve-3d;
}