Initial attempt to break media carousel out separately from viewer.
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
import { CSSResultGroup, unsafeCSS } from 'lit';
|
||||
import { EmblaCarouselType } from 'embla-carousel';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
import { FrigateCardCarousel } from './carousel.js';
|
||||
import type { MediaShowInfo } from '../types.js';
|
||||
import {
|
||||
dispatchExistingMediaShowInfoAsEvent,
|
||||
isValidMediaShowInfo,
|
||||
} from '../common.js';
|
||||
|
||||
import './next-prev-control.js';
|
||||
|
||||
import mediaCarouselStyle from '../scss/media-carousel.scss';
|
||||
|
||||
// TODO Remove this if not needed (and below)
|
||||
// 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`;
|
||||
// const IMG_EMPTY = getEmptyImageSrc(16, 9);
|
||||
|
||||
@customElement('frigate-card-media-carousel')
|
||||
export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
// A "map" from slide number to MediaShowInfo object.
|
||||
protected _mediaShowInfo: Record<number, MediaShowInfo> = {};
|
||||
|
||||
// Whether or not a given slide has been successfully lazily loaded.
|
||||
protected _slideHasBeenLazyLoaded: Record<number, boolean> = {};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* should lazy load, etc. `null` means lazy loading is disabled and everything
|
||||
* should load simultaneously.
|
||||
* @returns
|
||||
*/
|
||||
protected _getLazyLoadCount(): number | null {
|
||||
// Defaults to fully-lazy loading.
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the carousel with "slides" (clips or snapshots).
|
||||
*/
|
||||
protected _loadCarousel(): void {
|
||||
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;
|
||||
carousel?.on('select', this._selectSlideSetViewHandler.bind(this));
|
||||
|
||||
carousel?.on('init', this._selectSlideMediaShowHandler.bind(this));
|
||||
carousel?.on('select', this._selectSlideMediaShowHandler.bind(this));
|
||||
|
||||
if (this._getLazyLoadCount() != null) {
|
||||
carousel?.on('init', this._lazyLoadMediaHandler.bind(this));
|
||||
carousel?.on('select', this._lazyLoadMediaHandler.bind(this));
|
||||
carousel?.on('resize', this._lazyLoadMediaHandler.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the user selecting a new slide in the carousel.
|
||||
*/
|
||||
protected _selectSlideSetViewHandler(): 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();
|
||||
} else if (direction == 'next') {
|
||||
this._carousel?.scrollNext();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily load media in the carousel.
|
||||
*/
|
||||
protected _lazyLoadMediaHandler(): void {
|
||||
if (!this._carousel) {
|
||||
return;
|
||||
}
|
||||
const lazyLoadCount = this._getLazyLoadCount();
|
||||
if (lazyLoadCount === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slides = this._carousel.slideNodes();
|
||||
const slidesInView = this._carousel.slidesInView(true);
|
||||
const slidesToLoad = new Set<number>();
|
||||
|
||||
const minSlide = Math.min(...slidesInView);
|
||||
const maxSlide = Math.max(...slidesInView);
|
||||
|
||||
// Lazily load 'lazyLoadCount' slides on either side of the slides in view.
|
||||
for (let i = 1; i <= lazyLoadCount && minSlide - i >= 0; i++) {
|
||||
slidesToLoad.add(minSlide - i);
|
||||
}
|
||||
slidesInView.forEach((index) => slidesToLoad.add(index));
|
||||
for (let i = 1; i <= lazyLoadCount && maxSlide + i < slides.length; i++) {
|
||||
slidesToLoad.add(maxSlide + i);
|
||||
}
|
||||
|
||||
slidesToLoad.forEach((index) => {
|
||||
// Only lazy load slides that are not already loaded.
|
||||
if (this._slideHasBeenLazyLoaded[index]) {
|
||||
return;
|
||||
}
|
||||
this._slideHasBeenLazyLoaded[index] = true;
|
||||
this._lazyLoadSlide(slides[index]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy load a slide.
|
||||
* @param _slide The slide to lazy load.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected _lazyLoadSlide(_slide: HTMLElement): void {
|
||||
// To be overridden in children.
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a media show event when a slide is selected.
|
||||
*/
|
||||
protected _selectSlideMediaShowHandler(): void {
|
||||
if (!this._carousel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._carousel.slidesInView(true).forEach((slideIndex) => {
|
||||
if (slideIndex in this._mediaShowInfo) {
|
||||
dispatchExistingMediaShowInfoAsEvent(this, this._mediaShowInfo[slideIndex]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a media-show event that is generated by a child component, saving the
|
||||
* contents for future use when the relevant slide is actually shown.
|
||||
* @param slideIndex The relevant slide index.
|
||||
* @param event The media-show event from the child component.
|
||||
*/
|
||||
protected _mediaShowEventHandler(
|
||||
slideIndex: number,
|
||||
event: CustomEvent<MediaShowInfo>,
|
||||
): 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
if (mediaShowInfo && isValidMediaShowInfo(mediaShowInfo)) {
|
||||
this._mediaShowInfo[slideIndex] = mediaShowInfo;
|
||||
if (this._carousel && this._carousel?.slidesInView(true).includes(slideIndex)) {
|
||||
dispatchExistingMediaShowInfoAsEvent(this, mediaShowInfo);
|
||||
}
|
||||
/**
|
||||
* Images need a width/height from initial load, and browsers will assume
|
||||
* that the aspect ratio of the initial dummy-image load will persist. In
|
||||
* lazy-loading, this can cause a 1x1 pixel dummy image to cause the
|
||||
* browser to assume all images will be square, so the whole carousel will
|
||||
* have the wrong aspect-ratio until every single image has been lazily
|
||||
* loaded. To avoid this, we use a 16:9 dummy image at first (most
|
||||
* likely?) and once the first piece of real media has been loaded, all
|
||||
* dummy images are replaced with dummy images that match the aspect ratio
|
||||
* of the real image. It still might be wrong, but it's the best option
|
||||
* available.
|
||||
*/
|
||||
// TODO remove this
|
||||
// const firstMediaLoad = !Object.keys(this._mediaShowInfo).length;
|
||||
// if (firstMediaLoad && this.viewerConfig.lazy_load) {
|
||||
// const replacementImageSrc = getEmptyImageSrc(
|
||||
// mediaShowInfo.width,
|
||||
// mediaShowInfo.height,
|
||||
// );
|
||||
|
||||
// this.renderRoot.querySelectorAll('.embla__container img').forEach((img) => {
|
||||
// const imageElement: HTMLImageElement = img as HTMLImageElement;
|
||||
// if (imageElement.src === IMG_EMPTY) {
|
||||
// imageElement.src = replacementImageSrc;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return [super.styles, unsafeCSS(mediaCarouselStyle)];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user