From 6c03934656e2c17c47ee667c091abfea5b870e9f Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Mon, 22 Nov 2021 17:07:33 -0800 Subject: [PATCH 01/13] Thumbnail carousel initial draft --- src/card.ts | 32 +------- src/components/viewer.ts | 140 ++++++++++++++++++++++++-------- src/patches/ha-camera-stream.ts | 13 ++- src/patches/ha-hls-player.ts | 20 ++++- src/scss/card.scss | 10 +-- src/scss/gallery.scss | 2 + src/scss/live.scss | 8 +- src/scss/message.scss | 2 + src/scss/viewer-core.scss | 89 ++++++++++++++++++++ src/scss/viewer.scss | 51 +----------- 10 files changed, 242 insertions(+), 125 deletions(-) create mode 100644 src/scss/viewer-core.scss diff --git a/src/card.ts b/src/card.ts index 520073a5..d2d42910 100644 --- a/src/card.ts +++ b/src/card.ts @@ -913,41 +913,13 @@ export class FrigateCard extends LitElement { */ protected render(): TemplateResult | void { const padding = this._getAspectRatioPadding(); - const outerStyle = {}, - innerStyle = {}; + const outerStyle = {}; // Padding to force a particular aspect ratio. if (padding != null) { outerStyle['padding-top'] = `${padding}%`; } - // Special hacky treatment required when: - // - // - It's in fullscreen mode - // - It's viewing a media item - // - And the aspect ratio of the media item < aspect ratio of the window - // - // Cannot seem to scale the video by height in CSS without actually styling - // the underlying video element (which there is no access to as it's buried - // past multiple shadow roots), so instead scale the width in terms of'vh' - // (viewport height) in proportion to the aspect-ratio of the media. - if ( - screenfull.isEnabled && - screenfull.isFullscreen && - this._view.isMediaView() && - this._mediaShowInfo && - this._mediaShowInfo.width / this._mediaShowInfo.height < - window.innerWidth / window.innerHeight - ) { - // If the menu is outside the media (i.e. above/below) allow space for it. - const allowance = ['above', 'below'].includes(this.config.menu.mode) - ? MENU_HEIGHT - : 0; - innerStyle['max-width'] = `calc(${ - (100 * this._mediaShowInfo.width) / this._mediaShowInfo.height - }vh - ${allowance}px )`; - } - const contentClasses = { 'frigate-card-contents': true, absolute: padding != null, @@ -965,7 +937,7 @@ export class FrigateCard extends LitElement { > ${this.config.menu.mode == 'above' ? this._renderMenu() : ''}
-
+
${this._frigateCameraName == undefined ? until( (async () => { diff --git a/src/components/viewer.ts b/src/components/viewer.ts index f54abb40..29023bfb 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -38,6 +38,7 @@ import { renderProgressIndicator } from '../components/message.js'; import './next-prev-control.js'; import viewerStyle from '../scss/viewer.scss'; +import viewerCoreStyle from '../scss/viewer-core.scss'; import { actionHandler } from '../action-handler-directive.js'; const getEmptyImageSrc = (width: number, height: number) => @@ -168,6 +169,7 @@ export class FrigateCardViewerCore extends LitElement { // Media carousel object. protected _carousel?: EmblaCarouselType; + protected _thumbnailCarousel?: EmblaCarouselType; protected _loadedCarousel = false; // Mapping of slide # to BrowseMediaSource child #. @@ -226,9 +228,40 @@ export class FrigateCardViewerCore extends LitElement { this._carousel.on('init', this._lazyLoadMediaHandler.bind(this)); this._carousel.on('select', this._lazyLoadMediaHandler.bind(this)); this._carousel.on('resize', this._lazyLoadMediaHandler.bind(this)); + + const thumbCarouselNode = this.renderRoot.querySelector( + '.embla-thumbnails__viewport', + ) as HTMLElement; + if (thumbCarouselNode) { + this._thumbnailCarousel = EmblaCarousel(thumbCarouselNode, { + containScroll: 'keepSnaps', + dragFree: true, + }); + + this._carousel.on('select', this._syncThumbnailCarousel.bind(this)); + this._thumbnailCarousel.on('init', this._syncThumbnailCarousel.bind(this)); + } } } + protected _syncThumbnailCarousel(): void { + if (!this._carousel || !this._thumbnailCarousel) { + return; + } + + const previous = this._carousel.previousScrollSnap(); + const selected = this._carousel.selectedScrollSnap(); + + this._thumbnailCarousel + .slideNodes() + [previous].classList.remove('main-carousel-selected'); + this._thumbnailCarousel + .slideNodes() + [selected].classList.add('main-carousel-selected'); + + this._thumbnailCarousel.scrollTo(selected); + } + /** * Get the previous and next true media items from the current view. * @returns A BrowseMediaNeighbors with indices and objects of true media @@ -467,55 +500,67 @@ export class FrigateCardViewerCore extends LitElement { } const slides: TemplateResult[] = []; + const thumbnails: TemplateResult[] = []; this._slideToChild = {}; for (let i = 0; i < this.view.target.children?.length; ++i) { const slide = this._renderMediaItem(this.view.target.children[i], slides.length); + const thumbnail = this._renderThumbnail( + this.view.target.children[i], + slides.length, + ); + if (slide) { this._slideToChild[slides.length] = i; slides.push(slide); } + if (thumbnail) { + thumbnails.push(thumbnail); + } } const neighbors = this._getMediaNeighbors(); - return html`
- ${neighbors && neighbors.previous - ? html` { - this._nextPreviousHandler('previous'); - }} - >` - : ``} -
+ return html`
+ ${neighbors && neighbors.previous + ? html` { + this._nextPreviousHandler('previous'); + }} + >` + : ``}
${slides}
+ ${neighbors && neighbors.next + ? html` { + this._nextPreviousHandler('next'); + }} + >` + : ``}
- ${neighbors && neighbors.next - ? html` { - this._nextPreviousHandler('next'); - }} - >` - : ``} -
`; +
+
+
${thumbnails}
+
+
`; } /** @@ -600,6 +645,33 @@ export class FrigateCardViewerCore extends LitElement { * @param mediaToRender The media item to render. * @returns A template or void if the item could not be rendered. */ + + protected _renderThumbnail( + mediaToRender: BrowseMediaSource, + slideIndex: number, + ): TemplateResult | void { + if (!BrowseMediaUtil.isTrueMedia(mediaToRender) || !mediaToRender.thumbnail) { + return; + } + return html`
+ { + if (!this._carousel || !this._thumbnailCarousel) { + return; + } else if (this._thumbnailCarousel.clickAllowed()) { + this._carousel.scrollTo(slideIndex); + } + }} + /> +
`; + } + protected _renderMediaItem( mediaToRender: BrowseMediaSource, slideIndex: number, @@ -702,6 +774,6 @@ export class FrigateCardViewerCore extends LitElement { * Get element styles. */ static get styles(): CSSResultGroup { - return unsafeCSS(viewerStyle); + return unsafeCSS(viewerCoreStyle); } } diff --git a/src/patches/ha-camera-stream.ts b/src/patches/ha-camera-stream.ts index c317a97a..6aec018a 100644 --- a/src/patches/ha-camera-stream.ts +++ b/src/patches/ha-camera-stream.ts @@ -9,7 +9,7 @@ // available as compilation time. // ==================================================================== -import { TemplateResult, html } from 'lit'; +import { TemplateResult, css, html } from 'lit'; import { customElement } from 'lit/decorators.js'; import { dispatchMediaShowEvent } from '../common.js'; @@ -75,5 +75,16 @@ customElements.whenDefined('ha-camera-stream').then(() => { : ''} `; } + + static get styles(): CSSResultGroup { + return [ + super.styles, + css` + :host { + width: 100%; + height: 100%; + }` + ]; + } } }); diff --git a/src/patches/ha-hls-player.ts b/src/patches/ha-hls-player.ts index bf988b76..4e8cbce5 100644 --- a/src/patches/ha-hls-player.ts +++ b/src/patches/ha-hls-player.ts @@ -9,8 +9,9 @@ // available as compilation time. // ==================================================================== -import { TemplateResult, html } from 'lit'; +import { TemplateResult, css, html } from 'lit'; import { customElement } from 'lit/decorators.js'; + import { dispatchMediaShowEvent, dispatchPauseEvent, @@ -44,5 +45,22 @@ customElements.whenDefined('ha-hls-player').then(() => { > `; } + + static get styles(): CSSResultGroup { + return [ + super.styles, + css` + :host { + width: 100%; + height: 100%; + } + video { + object-fit: contain; + height: 100%; + width: 100%; + } + ` + ] + } } }); diff --git a/src/scss/card.scss b/src/scss/card.scss index 2a1d6445..8bf73f30 100644 --- a/src/scss/card.scss +++ b/src/scss/card.scss @@ -47,7 +47,8 @@ /* A relative div to place absolute picture elements onto */ .picture-elements { position: relative; - width: inherit; + width: 100%; + height: 100%; } /* Enforce picture elements to only be the size of the card/fullscreen (and not @@ -68,13 +69,6 @@ ha-card { background-color: var(--secondary-background-color, black); } -frigate-card-gallery, frigate-card-viewer, frigate-card-live, frigate-card-message, frigate-card-error-message { - width: 100%; - display: block; -} -frigate-card-gallery { - height: 100%; -} frigate-card-gallery.hidden,frigate-card-viewer.hidden,frigate-card-live.hidden { display: none; } diff --git a/src/scss/gallery.scss b/src/scss/gallery.scss index 9df685f1..9e7220c7 100644 --- a/src/scss/gallery.scss +++ b/src/scss/gallery.scss @@ -3,6 +3,8 @@ :host { display: block; + width: 100%; + height: 100%; overflow: auto; -ms-overflow-style: none; /* Hide scrollbar: IE and Edge */ scrollbar-width: none; /* Hide scrollbar: Firefox */ diff --git a/src/scss/live.scss b/src/scss/live.scss index 13a89892..e1b6f47e 100644 --- a/src/scss/live.scss +++ b/src/scss/live.scss @@ -1,10 +1,16 @@ :host { + width: 100%; + height: 100%; + display: block; + --video-max-height: none; } canvas { - width: 100%; display: block; + width: 100%; + height: 100%; + object-fit: contain; } /* Don't drop shadow or have radius for nested webrtc card */ diff --git a/src/scss/message.scss b/src/scss/message.scss index 54727410..21181063 100644 --- a/src/scss/message.scss +++ b/src/scss/message.scss @@ -1,5 +1,7 @@ :host { height: 100%; + width: 100%; + display: block; } .message { diff --git a/src/scss/viewer-core.scss b/src/scss/viewer-core.scss new file mode 100644 index 00000000..e4eb8176 --- /dev/null +++ b/src/scss/viewer-core.scss @@ -0,0 +1,89 @@ +:host { + display: flex; + flex-direction: column; + + height: 100%; + width: 100%; + + --video-max-height: none; + --frigate-card-viewer-thumbnail-size: 100px; +} + +img,video { + width: 100%; + height: 100%; + display: block; +} + +.embla, .embla-thumbnails { + 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; + min-height: 0; +} +.embla-thumbnails { + flex: 0 0 var(--frigate-card-viewer-thumbnail-size); + margin-top: 5px; + //margin-bottom: 5px; +} + +.embla__container, .embla-thumbnails__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, .embla-thumbnails__viewport { + width: 100%; + height: 100%; + overflow: hidden; +} +.embla__viewport.is-draggable, .embla-thumbnails__viewport.is-draggable { + cursor: move; + cursor: grab; +} +.embla__viewport.is-dragging, .embla-thumbnails__viewport.is-dragging { + cursor: grabbing; +} + +.embla__slide, .embla-thumbnails__slide { + position: relative; + height: 100%; + margin-right: 5px; + overflow: hidden; +} +.embla__slide { + flex: 0 0 100%; +} +.embla-thumbnails__slide { + flex: 0 0 var(--frigate-card-viewer-thumbnail-size); + border-radius: 5px; + opacity: 0.4; + transition: opacity 0.2s; +} +.embla-thumbnails__slide.main-carousel-selected { + opacity: 1.0; +} + +.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 diff --git a/src/scss/viewer.scss b/src/scss/viewer.scss index e9359202..e792d174 100644 --- a/src/scss/viewer.scss +++ b/src/scss/viewer.scss @@ -1,54 +1,5 @@ :host { - --video-max-height: none; -} - -ha-hls-player { - transform-style: preserve-3d; -} - -img,video { + height: 100%; width: 100%; - height: auto; display: block; } - -div.container { - // Keep the controls positioned relative to the video. - position: relative; -} - -.embla { - position: relative; - margin-left: auto; - margin-right: auto; -} - -.embla__viewport { - overflow: hidden; - width: 100%; -} - -.embla__container { - display: flex; - user-select: none; - -webkit-touch-callout: none; - -khtml-user-select: none; - -webkit-tap-highlight-color: transparent; -} - -.embla__viewport.is-draggable { - cursor: move; - cursor: grab; -} - -.embla__viewport.is-dragging { - cursor: grabbing; -} - -.embla__slide { - position: relative; - min-width: 100%; - max-width: 100%; - margin-right: 10px; - overflow: hidden; -} \ No newline at end of file From 48f288e0fd741e674a05ae79f8f5225377387fb9 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 23 Nov 2021 21:53:53 -0800 Subject: [PATCH 02/13] Break carousel out into its own component. --- src/card.ts | 1 - src/components/carousel.ts | 103 +++++++++++++++++++++++++++ src/components/menu.ts | 1 - src/components/thumbnail-carousel.ts | 102 ++++++++++++++++++++++++++ src/components/viewer.ts | 90 +++++------------------ src/scss/carousel.scss | 53 ++++++++++++++ src/scss/thumbnail-carousel.scss | 14 ++++ src/scss/viewer-core.scss | 25 ++----- 8 files changed, 298 insertions(+), 91 deletions(-) create mode 100644 src/components/carousel.ts create mode 100644 src/components/thumbnail-carousel.ts create mode 100644 src/scss/carousel.scss create mode 100644 src/scss/thumbnail-carousel.scss diff --git a/src/card.ts b/src/card.ts index d2d42910..ba9fe988 100644 --- a/src/card.ts +++ b/src/card.ts @@ -43,7 +43,6 @@ import { CARD_VERSION, REPO_URL } from './const.js'; import { FrigateCardElements } from './components/elements.js'; import { FRIGATE_BUTTON_MENU_ICON, - MENU_HEIGHT, FrigateCardMenu, } from './components/menu.js'; import { View } from './view.js'; diff --git a/src/components/carousel.ts b/src/components/carousel.ts new file mode 100644 index 00000000..b7007314 --- /dev/null +++ b/src/components/carousel.ts @@ -0,0 +1,103 @@ +import { + CSSResultGroup, + LitElement, + TemplateResult, + html, + unsafeCSS, + PropertyValues, +} from 'lit'; +import EmblaCarousel, { EmblaCarouselType, EmblaOptionsType } from 'embla-carousel'; + +import { dispatchFrigateCardEvent } from '../common'; + +import carouselStyle from '../scss/carousel.scss'; + +export interface CarouselTap { + index: number; +} +export interface CarouselSelect { + index: number; +} + +export class FrigateCardCarousel extends LitElement { + protected _options?: EmblaOptionsType; + protected _carousel?: EmblaCarouselType; + + /** + * Scroll to a particular slide. + * @param index Slide number. + */ + carouselScrollTo(index: number): void { + this._carousel?.scrollTo(index); + } + + /** + * 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._carousel) { + this.updateComplete.then(() => { + this._loadCarousel(); + }); + } + } + + /** + * Get slides to include in the render. + * @returns The slides to include in the render. + */ + protected _getSlides(): TemplateResult[] { + return []; + } + + /** + * Load the carousel with "slides". + */ + protected _loadCarousel(): void { + const carouselNode = this.renderRoot.querySelector( + '.embla__viewport', + ) as HTMLElement; + + if (carouselNode && !this._carousel) { + 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) { + dispatchFrigateCardEvent(this, 'carousel:select', { + index: this._carousel.selectedScrollSnap(), + }); + } + }); + } + } + + /** + * Render the element. + * @returns A template to display to the user. + */ + protected render(): TemplateResult | void { + const slides = this._getSlides(); + if (!slides) { + return; + } + + return html`
+
+
${slides}
+
+
`; + } + + /** + * Get element styles. + */ + static get styles(): CSSResultGroup { + return unsafeCSS(carouselStyle); + } +} diff --git a/src/components/menu.ts b/src/components/menu.ts index 29625e4e..5aacc472 100644 --- a/src/components/menu.ts +++ b/src/components/menu.ts @@ -28,7 +28,6 @@ import { import menuStyle from '../scss/menu.scss'; import { ConditionState, evaluateCondition } from '../card-condition.js'; -export const MENU_HEIGHT = 46; export const FRIGATE_BUTTON_MENU_ICON = 'frigate'; /** diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts new file mode 100644 index 00000000..0e5bc73e --- /dev/null +++ b/src/components/thumbnail-carousel.ts @@ -0,0 +1,102 @@ +import { BrowseMediaUtil } from '../browse-media-util.js'; +import { CSSResultGroup, TemplateResult, html, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +import type { BrowseMediaSource } from '../types.js'; +import { CarouselTap, FrigateCardCarousel } from './carousel.js'; +import { actionHandler } from '../action-handler-directive.js'; +import { dispatchFrigateCardEvent } from '../common.js'; + +import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss'; + +@customElement('frigate-card-thumbnail-carousel') +export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { + @property({ attribute: false }) + protected target?: BrowseMediaSource; + + protected _tapSelected? = 0; + + constructor() { + super(); + this._options = { + containScroll: 'keepSnaps', + dragFree: true, + }; + } + + /** + * Scroll to a particular slide. + * @param index Slide number. + */ + carouselScrollTo(index: number): void { + if (!this._carousel) { + return; + } + + if (this._tapSelected !== undefined) { + this._carousel.slideNodes()[this._tapSelected].classList.remove("slide-selected"); + } + + super.carouselScrollTo(index); + + this._carousel.slideNodes()[index].classList.add("slide-selected"); + this._tapSelected = index; + } + + /** + * Get slides to include in the render. + * @returns The slides to include in the render. + */ + protected _getSlides(): TemplateResult[] { + if (!this.target || !this.target.children || !this.target.children.length) { + return []; + } + + const slides: TemplateResult[] = []; + for (let i = 0; i < this.target.children.length; ++i) { + const thumbnail = this._renderThumbnail(this.target.children[i], slides.length); + if (thumbnail) { + slides.push(thumbnail); + } + } + return slides; + } + + /** + * Render a given thumbnail. + * @param mediaToRender The media item to render. + * @returns A template or void if the item could not be rendered. + */ + protected _renderThumbnail( + mediaToRender: BrowseMediaSource, + slideIndex: number, + ): TemplateResult | void { + if (!BrowseMediaUtil.isTrueMedia(mediaToRender) || !mediaToRender.thumbnail) { + return; + } + + return html`
{ + if (this._carousel && this._carousel.clickAllowed()) { + dispatchFrigateCardEvent(this, 'carousel:tap', { + index: slideIndex + }); + } + }} + > + +
`; + } + + /** + * Get element styles. + */ + static get styles(): CSSResultGroup { + return [super.styles, unsafeCSS(thumbnailCarouselStyle)]; + } +} diff --git a/src/components/viewer.ts b/src/components/viewer.ts index 29023bfb..76c4caa2 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -9,6 +9,7 @@ import { import { BrowseMediaUtil } from '../browse-media-util.js'; import EmblaCarousel, { 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'; import { ifDefined } from 'lit-html/directives/if-defined.js'; import { until } from 'lit/directives/until.js'; @@ -21,8 +22,11 @@ import type { MediaShowInfo, ViewerConfig, } from '../types.js'; +import { CarouselTap } from './carousel.js'; +import { FrigateCardThumbnailCarousel } from './thumbnail-carousel.js'; import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js'; import { View } from '../view.js'; +import { actionHandler } from '../action-handler-directive.js'; import { createMediaShowInfo, dispatchErrorMessageEvent, @@ -36,10 +40,10 @@ import { localize } from '../localize/localize.js'; import { renderProgressIndicator } from '../components/message.js'; import './next-prev-control.js'; +import './thumbnail-carousel.js'; import viewerStyle from '../scss/viewer.scss'; import viewerCoreStyle from '../scss/viewer-core.scss'; -import { actionHandler } from '../action-handler-directive.js'; 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`; @@ -169,9 +173,10 @@ export class FrigateCardViewerCore extends LitElement { // Media carousel object. protected _carousel?: EmblaCarouselType; - protected _thumbnailCarousel?: EmblaCarouselType; protected _loadedCarousel = false; + protected _thumbnailCarouselRef: Ref = createRef(); + // Mapping of slide # to BrowseMediaSource child #. // (Folders are not media items that can be rendered). protected _slideToChild: Record = {}; @@ -229,37 +234,17 @@ export class FrigateCardViewerCore extends LitElement { this._carousel.on('select', this._lazyLoadMediaHandler.bind(this)); this._carousel.on('resize', this._lazyLoadMediaHandler.bind(this)); - const thumbCarouselNode = this.renderRoot.querySelector( - '.embla-thumbnails__viewport', - ) as HTMLElement; - if (thumbCarouselNode) { - this._thumbnailCarousel = EmblaCarousel(thumbCarouselNode, { - containScroll: 'keepSnaps', - dragFree: true, - }); - - this._carousel.on('select', this._syncThumbnailCarousel.bind(this)); - this._thumbnailCarousel.on('init', this._syncThumbnailCarousel.bind(this)); - } + this._carousel.on('select', this._syncThumbnailCarousel.bind(this)); } } protected _syncThumbnailCarousel(): void { - if (!this._carousel || !this._thumbnailCarousel) { + if (!this._carousel) { return; } - const previous = this._carousel.previousScrollSnap(); - const selected = this._carousel.selectedScrollSnap(); - - this._thumbnailCarousel - .slideNodes() - [previous].classList.remove('main-carousel-selected'); - this._thumbnailCarousel - .slideNodes() - [selected].classList.add('main-carousel-selected'); - - this._thumbnailCarousel.scrollTo(selected); + this._thumbnailCarouselRef.value?.carouselScrollTo( + this._carousel.selectedScrollSnap()); } /** @@ -500,23 +485,15 @@ export class FrigateCardViewerCore extends LitElement { } const slides: TemplateResult[] = []; - const thumbnails: TemplateResult[] = []; this._slideToChild = {}; for (let i = 0; i < this.view.target.children?.length; ++i) { const slide = this._renderMediaItem(this.view.target.children[i], slides.length); - const thumbnail = this._renderThumbnail( - this.view.target.children[i], - slides.length, - ); if (slide) { this._slideToChild[slides.length] = i; slides.push(slide); } - if (thumbnail) { - thumbnails.push(thumbnail); - } } const neighbors = this._getMediaNeighbors(); @@ -556,11 +533,14 @@ export class FrigateCardViewerCore extends LitElement { >` : ``}
-
-
-
${thumbnails}
-
-
`; + ) => this._carousel?.scrollTo(ev.detail.index)} + @frigate-card:carousel:init=${this._syncThumbnailCarousel.bind(this)} + > + `; } /** @@ -640,38 +620,6 @@ export class FrigateCardViewerCore extends LitElement { } } - /** - * Render a given media item. - * @param mediaToRender The media item to render. - * @returns A template or void if the item could not be rendered. - */ - - protected _renderThumbnail( - mediaToRender: BrowseMediaSource, - slideIndex: number, - ): TemplateResult | void { - if (!BrowseMediaUtil.isTrueMedia(mediaToRender) || !mediaToRender.thumbnail) { - return; - } - return html`
- { - if (!this._carousel || !this._thumbnailCarousel) { - return; - } else if (this._thumbnailCarousel.clickAllowed()) { - this._carousel.scrollTo(slideIndex); - } - }} - /> -
`; - } - protected _renderMediaItem( mediaToRender: BrowseMediaSource, slideIndex: number, diff --git a/src/scss/carousel.scss b/src/scss/carousel.scss new file mode 100644 index 00000000..0ecdb6d4 --- /dev/null +++ b/src/scss/carousel.scss @@ -0,0 +1,53 @@ +:host { + display: block; + height: 100%; + width: 100%; +} + +img,video { + width: 100%; + height: 100%; + display: block; +} + +.embla { + position: relative; + margin-left: auto; + margin-right: auto; +} + +.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: visible; +} +.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/thumbnail-carousel.scss b/src/scss/thumbnail-carousel.scss new file mode 100644 index 00000000..aca8fe51 --- /dev/null +++ b/src/scss/thumbnail-carousel.scss @@ -0,0 +1,14 @@ +.embla__slide { + flex: 0 0 var(--frigate-card-viewer-thumbnail-size); + opacity: 0.4; + transition: opacity 1s ease, transform 0.3s ease; +} +.embla__slide.slide-selected { + opacity: 1.0; +} +.embla__slide:hover { + transform: scale(1.1); +} +.embla__slide img { + border-radius: 5px; +} \ No newline at end of file diff --git a/src/scss/viewer-core.scss b/src/scss/viewer-core.scss index e4eb8176..83bc822b 100644 --- a/src/scss/viewer-core.scss +++ b/src/scss/viewer-core.scss @@ -15,7 +15,7 @@ img,video { display: block; } -.embla, .embla-thumbnails { +.embla { position: relative; margin-left: auto; margin-right: auto; @@ -27,13 +27,12 @@ img,video { flex: 1; min-height: 0; } -.embla-thumbnails { +frigate-card-thumbnail-carousel { flex: 0 0 var(--frigate-card-viewer-thumbnail-size); margin-top: 5px; - //margin-bottom: 5px; } -.embla__container, .embla-thumbnails__container { +.embla__container { display: flex; width: 100%; height: 100%; @@ -44,20 +43,20 @@ img,video { -webkit-tap-highlight-color: transparent; } -.embla__viewport, .embla-thumbnails__viewport { +.embla__viewport { width: 100%; height: 100%; overflow: hidden; } -.embla__viewport.is-draggable, .embla-thumbnails__viewport.is-draggable { +.embla__viewport.is-draggable { cursor: move; cursor: grab; } -.embla__viewport.is-dragging, .embla-thumbnails__viewport.is-dragging { +.embla__viewport.is-dragging { cursor: grabbing; } -.embla__slide, .embla-thumbnails__slide { +.embla__slide { position: relative; height: 100%; margin-right: 5px; @@ -66,16 +65,6 @@ img,video { .embla__slide { flex: 0 0 100%; } -.embla-thumbnails__slide { - flex: 0 0 var(--frigate-card-viewer-thumbnail-size); - border-radius: 5px; - opacity: 0.4; - transition: opacity 0.2s; -} -.embla-thumbnails__slide.main-carousel-selected { - opacity: 1.0; -} - .embla__slide img,video { // Letterbox media. has similar added directly in // its element. From 2e9be783a657acd57479fa043e29507e859fc274 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Wed, 24 Nov 2021 13:46:23 -0800 Subject: [PATCH 03/13] Convert viewer to inherit from media carousel. --- src/components/carousel.ts | 18 ++- src/components/thumbnail-carousel.ts | 2 +- src/components/viewer.ts | 162 ++++++++++++++++----------- src/scss/media-carousel.scss | 13 +++ src/scss/thumbnail-carousel.scss | 4 + src/scss/viewer-core.scss | 65 +---------- 6 files changed, 126 insertions(+), 138 deletions(-) create mode 100644 src/scss/media-carousel.scss 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 From 193dcf54804b8d55abb15e4cd8a79327bd05fd7e Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Wed, 24 Nov 2021 13:52:13 -0800 Subject: [PATCH 04/13] Add missing height on embla class. --- src/scss/carousel.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/scss/carousel.scss b/src/scss/carousel.scss index 0ecdb6d4..66ea1971 100644 --- a/src/scss/carousel.scss +++ b/src/scss/carousel.scss @@ -11,6 +11,8 @@ img,video { } .embla { + width: 100%; + height: 100%; position: relative; margin-left: auto; margin-right: auto; From bb36004b6d0e0612e69ffddf906396af446b483b Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Wed, 24 Nov 2021 14:42:41 -0800 Subject: [PATCH 05/13] Support the thumbnail carousel above or below. --- README.md | 6 +- src/components/thumbnail-carousel.ts | 19 +++-- src/components/viewer.ts | 121 ++++++++++++++------------- src/scss/thumbnail-carousel.scss | 2 +- src/types.ts | 15 ++++ 5 files changed, 98 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index e533a7cb..32406aa2 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ All variables listed are under a `menu:` section. | Option | Default | Description | | - | - | - | | `mode` | `hidden-top` | The menu mode to show by default. See [menu modes](#menu-modes) below.| -| `button_size` | `40px` | The size of the menu buttons (in CSS Units)[https://www.w3schools.com/cssref/css_units.asp].| +| `button_size` | `40px` | The size of the menu buttons [in CSS Units](https://www.w3schools.com/cssref/css_units.asp).| | `buttons.{frigate, live, clips, snapshots, image, download, frigate_ui, fullscreen}` | `true`, except for `image` | Whether or not to show these builtin actions in the card menu. | | `conditions` | | Condition(s) that must be met in order for the menu to be displayed. These conditions use the same format as the `custom:frigate-card-conditional` card (see [Possible conditions](#frigate-card-conditions) below). If conditions are specified but not met, then the menu is not rendered.| @@ -136,7 +136,9 @@ The `event_viewer` is used for viewing all `clip` and `snapshot` media, in a med | `lazy_load` | `true` | Whether or not to lazily load media in the event viewer carousel. Setting this will false will fetch all media immediately which may make the carousel experience smoother at a cost of (potentially) a substantial number of simultaneous media fetches on load. | | `draggable` | `true` | Whether or not the event viewer carousel can be dragged left or right, via touch/swipe and mouse dragging. | | `controls.next_previous.style` | `thumbnails` | When viewing media, what kind of controls to show to move to the previous/next media item. Acceptable values: `thumbnails`, `chevrons`, `none` . | -| `controls.next_previous.size` | `48px` | The size of the next/previous controls (in CSS Units)[https://www.w3schools.com/cssref/css_units.asp].| +| `controls.next_previous.size` | `48px` | The size of the next/previous controls [in CSS Units](https://www.w3schools.com/cssref/css_units.asp).| +| `controls.thumbnails.mode` | `below` | Whether to show the thumbnail carousel `below` the media, `above` the media or to hide it entirely (`none`).| +| `controls.thumbnails.size` | `100px` | The size of the thumbnails in the thumbnail carousel [in CSS Units](https://www.w3schools.com/cssref/css_units.asp).| | `actions` | | Actions to use for all views that use the `event_viewer` (e.g. `clip`, `snapshot`). See [actions](#actions) below.| ### Event Gallery options diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts index 3438ab39..9ba3b2e7 100644 --- a/src/components/thumbnail-carousel.ts +++ b/src/components/thumbnail-carousel.ts @@ -2,7 +2,7 @@ import { BrowseMediaUtil } from '../browse-media-util.js'; import { CSSResultGroup, TemplateResult, html, unsafeCSS } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import type { BrowseMediaSource } from '../types.js'; +import type { BrowseMediaSource, ThumbnailsControlConfig } from '../types.js'; import { CarouselTap, FrigateCardCarousel } from './carousel.js'; import { actionHandler } from '../action-handler-directive.js'; import { dispatchFrigateCardEvent } from '../common.js'; @@ -16,6 +16,15 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { protected _tapSelected?; + @property({ attribute: false }) + set config(config: ThumbnailsControlConfig) { + this._config = config; + if (config) { + this.style.setProperty('--frigate-card-viewer-thumbnail-size', config.size); + } + } + public _config?: ThumbnailsControlConfig; + constructor() { super(); this._options = { @@ -34,12 +43,12 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { } if (this._tapSelected !== undefined) { - this._carousel.slideNodes()[this._tapSelected].classList.remove("slide-selected"); + this._carousel.slideNodes()[this._tapSelected].classList.remove('slide-selected'); } super.carouselScrollTo(index); - this._carousel.slideNodes()[index].classList.add("slide-selected"); + this._carousel.slideNodes()[index].classList.add('slide-selected'); this._tapSelected = index; } @@ -84,12 +93,12 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { @action=${() => { if (this._carousel && this._carousel.clickAllowed()) { dispatchFrigateCardEvent(this, 'carousel:tap', { - index: slideIndex + index: slideIndex, }); } }} > - +
`; } diff --git a/src/components/viewer.ts b/src/components/viewer.ts index 0797e734..a4b63658 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -1,10 +1,4 @@ -import { - CSSResultGroup, - LitElement, - TemplateResult, - html, - unsafeCSS, -} from 'lit'; +import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; import { BrowseMediaUtil } from '../browse-media-util.js'; import { EmblaCarouselType } from 'embla-carousel'; import { HomeAssistant } from 'custom-card-helpers'; @@ -181,11 +175,31 @@ export class FrigateCardViewerCore extends LitElement { } } - protected render(): TemplateResult | void { - if (!this.view) { + protected _renderThumbnails(): TemplateResult { + if (!this.view || !this.viewerConfig) { return html``; } - return html` + + return html` ) => { + this._mediaCarouselRef.value?.carouselScrollTo(ev.detail.index); + }} + @frigate-card:carousel:init=${this._syncThumbnailCarousel.bind(this)} + > + `; + } + + protected render(): TemplateResult | void { + if (!this.view || !this.viewerConfig) { + return html``; + } + return html` ${this.viewerConfig && + this.viewerConfig.controls.thumbnails.mode === 'above' + ? this._renderThumbnails() + : ''} - ) => { - this._mediaCarouselRef.value?.carouselScrollTo(ev.detail.index); - }} - @frigate-card:carousel:init=${this._syncThumbnailCarousel.bind(this)} - > - `; + ${this.viewerConfig && this.viewerConfig.controls.thumbnails.mode === 'below' + ? this._renderThumbnails() + : ''}`; } /** * Get element styles. */ - static get styles(): CSSResultGroup { + static get styles(): CSSResultGroup { return unsafeCSS(viewerCoreStyle); } } - @customElement('frigate-card-media-carousel') export class FrigateCardMediaCarousel extends FrigateCardCarousel { @property({ attribute: false }) @@ -250,7 +257,7 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { if (this._carousel || !this.viewerConfig) { return; } - + // Start the carousel on the selected child number. const startIndex = Number( Object.keys(this._slideToChild).find( @@ -261,7 +268,7 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { this._options = { startIndex: isNaN(startIndex) ? undefined : startIndex, draggable: this.viewerConfig.draggable, - } + }; super._loadCarousel(); @@ -533,40 +540,40 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { const neighbors = this._getMediaNeighbors(); return html`
- ${neighbors && neighbors.previous - ? html` { - this._nextPreviousHandler('previous'); - }} - >` - : ``} -
-
${slides}
-
- ${neighbors && neighbors.next - ? html` { - this._nextPreviousHandler('next'); - }} - >` - : ``} -
`; + ${neighbors && neighbors.previous + ? html` { + this._nextPreviousHandler('previous'); + }} + >` + : ``} +
+
${slides}
+
+ ${neighbors && neighbors.next + ? html` { + this._nextPreviousHandler('next'); + }} + >` + : ``} + `; } /** diff --git a/src/scss/thumbnail-carousel.scss b/src/scss/thumbnail-carousel.scss index 6dd52e2d..262b9c1a 100644 --- a/src/scss/thumbnail-carousel.scss +++ b/src/scss/thumbnail-carousel.scss @@ -5,7 +5,7 @@ .embla__slide { flex: 0 0 var(--frigate-card-viewer-thumbnail-size); opacity: 0.4; - transition: opacity 1s ease, transform 0.3s ease; + transition: opacity 0.6s ease, transform 0.3s ease; } .embla__slide.slide-selected { opacity: 1.0; diff --git a/src/types.ts b/src/types.ts index 68d2e67d..f94e79c2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -454,6 +454,10 @@ const viewerConfigDefault = { size: '48px', style: 'thumbnails' as const, }, + thumbnails: { + size: '100px', + mode: 'below' as const, + }, }, }; const nextPreviousControlConfigSchema = z @@ -466,6 +470,16 @@ const nextPreviousControlConfigSchema = z .default(viewerConfigDefault.controls.next_previous); export type NextPreviousControlConfig = z.infer; +const thumbnailsControlConfigSchema = z + .object({ + mode: z + .enum(['none', 'above', 'below']) + .default(viewerConfigDefault.controls.thumbnails.mode), + size: z.string().default(viewerConfigDefault.controls.thumbnails.size), + }) + .default(viewerConfigDefault.controls.thumbnails); +export type ThumbnailsControlConfig = z.infer; + const viewerConfigSchema = z .object({ autoplay_clip: z.boolean().default(viewerConfigDefault.autoplay_clip), @@ -474,6 +488,7 @@ const viewerConfigSchema = z controls: z .object({ next_previous: nextPreviousControlConfigSchema, + thumbnails: thumbnailsControlConfigSchema, }) .default(viewerConfigDefault.controls), }) From cbd2505b98efc9b8f04d402fb2253e0f77ffd295 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Wed, 24 Nov 2021 17:49:39 -0800 Subject: [PATCH 06/13] Minor refactor for consistency. --- src/components/carousel.ts | 27 --------------------------- src/components/menu.ts | 2 +- src/components/thumbnail-carousel.ts | 19 ++++++++++++++++++- src/components/viewer.ts | 27 ++++++++++++++++++--------- 4 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/components/carousel.ts b/src/components/carousel.ts index 955efcac..00ff0b1b 100644 --- a/src/components/carousel.ts +++ b/src/components/carousel.ts @@ -1,8 +1,6 @@ import { CSSResultGroup, LitElement, - TemplateResult, - html, unsafeCSS, PropertyValues, } from 'lit'; @@ -53,14 +51,6 @@ export class FrigateCardCarousel extends LitElement { } } - /** - * Get slides to include in the render. - * @returns The slides to include in the render. - */ - protected _getSlides(): TemplateResult[] { - return []; - } - /** * Load the carousel with "slides". */ @@ -83,23 +73,6 @@ export class FrigateCardCarousel extends LitElement { } } - /** - * Render the element. - * @returns A template to display to the user. - */ - protected render(): TemplateResult | void { - const slides = this._getSlides(); - if (!slides) { - return; - } - - return html`
-
-
${slides}
-
-
`; - } - /** * Get element styles. */ diff --git a/src/components/menu.ts b/src/components/menu.ts index 5aacc472..0b8bd3ef 100644 --- a/src/components/menu.ts +++ b/src/components/menu.ts @@ -45,7 +45,7 @@ export class FrigateCardMenu extends LitElement { this.style.setProperty('--frigate-card-menu-button-size', menuConfig.button_size); } } - public _menuConfig?: MenuConfig; + protected _menuConfig?: MenuConfig; @property({ attribute: false }) protected expand = false; diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts index 9ba3b2e7..c7f43e08 100644 --- a/src/components/thumbnail-carousel.ts +++ b/src/components/thumbnail-carousel.ts @@ -23,7 +23,7 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { this.style.setProperty('--frigate-card-viewer-thumbnail-size', config.size); } } - public _config?: ThumbnailsControlConfig; + protected _config?: ThumbnailsControlConfig; constructor() { super(); @@ -102,6 +102,23 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { `; } + /** + * Render the element. + * @returns A template to display to the user. + */ + protected render(): TemplateResult | void { + const slides = this._getSlides(); + if (!slides || !this._config || this._config.mode == 'none') { + return; + } + + return html`
+
+
${slides}
+
+
`; + } + /** * Get element styles. */ diff --git a/src/components/viewer.ts b/src/components/viewer.ts index a4b63658..cd2fa7c8 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -510,24 +510,21 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { } /** - * Render the element. - * @returns A template to display to the user. + * Get slides to include in the render. + * @returns The slides to include in the render. */ - protected render(): TemplateResult | void { + protected _getSlides(): TemplateResult[] { if ( !this.view || !this.view.target || !this.view.target.children || - !this.view.target.children.length || - this.view.childIndex === undefined || - !this.resolvedMediaCache + !this.view.target.children.length ) { - return html``; + return []; } - const slides: TemplateResult[] = []; this._slideToChild = {}; - + const slides: TemplateResult[] = []; for (let i = 0; i < this.view.target.children?.length; ++i) { const slide = this._renderMediaItem(this.view.target.children[i], slides.length); @@ -536,6 +533,18 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { slides.push(slide); } } + return slides; + } + + /** + * Render the element. + * @returns A template to display to the user. + */ + protected render(): TemplateResult | void { + const slides = this._getSlides(); + if (!slides) { + return; + } const neighbors = this._getMediaNeighbors(); From 3c7634d9d5c0f2117d9e326c94e16015a93e2fa8 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 25 Nov 2021 15:22:41 -0800 Subject: [PATCH 07/13] Add thumbnail to live view. --- src/card.ts | 30 +++++--- src/components/carousel.ts | 3 - src/components/live.ts | 94 ++++++++++++++++------- src/components/thumbnail-carousel.ts | 107 ++++++++++++++++++++++++--- src/components/viewer.ts | 15 ++-- src/scss/live-frigate.scss | 7 ++ src/scss/live-jsmpeg.scss | 12 +++ src/scss/live-webrtc.scss | 12 +++ src/scss/live.scss | 21 ++---- src/scss/thumbnail-carousel.scss | 12 ++- src/scss/viewer-core.scss | 6 +- src/types.ts | 46 +++++++++--- 12 files changed, 273 insertions(+), 92 deletions(-) create mode 100644 src/scss/live-frigate.scss create mode 100644 src/scss/live-jsmpeg.scss create mode 100644 src/scss/live-webrtc.scss diff --git a/src/card.ts b/src/card.ts index ba9fe988..0fa946f0 100644 --- a/src/card.ts +++ b/src/card.ts @@ -41,10 +41,7 @@ import type { import { CARD_VERSION, REPO_URL } from './const.js'; import { FrigateCardElements } from './components/elements.js'; -import { - FRIGATE_BUTTON_MENU_ICON, - FrigateCardMenu, -} from './components/menu.js'; +import { FRIGATE_BUTTON_MENU_ICON, FrigateCardMenu } from './components/menu.js'; import { View } from './view.js'; import { convertActionToFrigateCardCustomAction, @@ -66,6 +63,7 @@ import './components/live.js'; import './components/menu.js'; import './components/message.js'; import './components/viewer.js'; +import './components/thumbnail-carousel.js'; import './patches/ha-camera-stream.js'; import './patches/ha-hls-player.js'; @@ -736,15 +734,21 @@ export class FrigateCard extends LitElement { * Get the parameters to search for media related to the current view. * @returns A BrowseMediaQueryParameters object. */ - protected _getBrowseMediaQueryParameters(): BrowseMediaQueryParameters | undefined { + protected _getBrowseMediaQueryParameters( + mediaType?: 'clips' | 'snapshots', + ): BrowseMediaQueryParameters | undefined { if ( !this._frigateCameraName || - !(this._view.isClipRelatedView() || this._view.isSnapshotRelatedView()) + !( + this._view.isClipRelatedView() || + this._view.isSnapshotRelatedView() || + mediaType + ) ) { return undefined; } return { - mediaType: this._view.isClipRelatedView() ? 'clips' : 'snapshots', + mediaType: mediaType || (this._view.isClipRelatedView() ? 'clips' : 'snapshots'), clientId: this.config.frigate.client_id, cameraName: this._frigateCameraName, label: this.config.frigate.label, @@ -968,7 +972,6 @@ export class FrigateCard extends LitElement { true, ); } - const mediaQueryParameters = this._getBrowseMediaQueryParameters(); const pictureElementsClasses = { 'picture-elements': true, @@ -1003,7 +1006,7 @@ export class FrigateCard extends LitElement { ? html` { - conditionStateRequestHandler(ev, this._conditionState) + conditionStateRequestHandler(ev, this._conditionState); }} > diff --git a/src/components/carousel.ts b/src/components/carousel.ts index 00ff0b1b..cd8da910 100644 --- a/src/components/carousel.ts +++ b/src/components/carousel.ts @@ -10,9 +10,6 @@ import { dispatchFrigateCardEvent } from '../common'; import carouselStyle from '../scss/carousel.scss'; -export interface CarouselTap { - index: number; -} export interface CarouselSelect { index: number; } diff --git a/src/components/live.ts b/src/components/live.ts index 264a3418..90b7b537 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -1,5 +1,6 @@ import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; import type { + BrowseMediaQueryParameters, ExtendedHomeAssistant, FrigateCardConfig, JSMPEGConfig, @@ -25,6 +26,12 @@ import { renderProgressIndicator } from '../components/message.js'; import JSMpeg from '@cycjimmy/jsmpeg-player'; import liveStyle from '../scss/live.scss'; +import liveFrigateStyle from '../scss/live-frigate.scss'; +import liveJSMPEGStyle from '../scss/live-jsmpeg.scss'; +import liveWebRTCStyle from '../scss/live-webrtc.scss'; + +import { View } from '../view.js'; +import { ThumbnailCarouselTap } from './thumbnail-carousel.js'; // Number of seconds a signed URL is valid for. const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60; @@ -41,7 +48,7 @@ export class FrigateCardLive extends LitElement { protected config?: FrigateCardConfig; @property({ attribute: false }) - protected frigateCameraName?: string; + protected browseMediaQueryParameters?: BrowseMediaQueryParameters; @property({ attribute: false }) set preload(preload: boolean) { @@ -72,6 +79,33 @@ export class FrigateCardLive extends LitElement { } } + /** + * Render thumbnails carousel. + * @returns A rendered template or void. + */ + protected renderThumbnails(): TemplateResult | void { + if (!this.config) { + return; + } + return html` ) => { + const mediaType = this.browseMediaQueryParameters?.mediaType; + if (mediaType && ['snapshots', 'clips'].includes(mediaType)) { + new View({ + view: mediaType === 'clips' ? 'clip-specific' : 'snapshot-specific', + target: ev.detail.target, + childIndex: ev.detail.childIndex, + }).dispatchChangeEvent(this); + } + }} + > + `; + } + /** * Master render method. * @returns A rendered template. @@ -81,28 +115,36 @@ export class FrigateCardLive extends LitElement { return; } - return html` ${this.config.live.provider == 'frigate' - ? html` - ` - : this.config.live.provider == 'webrtc' - ? html` - ` - : html` - `}`; + return html` + ${this.config.live.controls.thumbnails.mode === 'above' + ? this.renderThumbnails() + : ''} + ${this.config.live.provider == 'frigate' + ? html` + ` + : this.config.live.provider == 'webrtc' + ? html` + ` + : html` + `} + ${this.config.live.controls.thumbnails.mode === 'below' + ? this.renderThumbnails() + : ''} + `; } /** @@ -150,7 +192,7 @@ export class FrigateCardLiveFrigate extends LitElement { * Get styles. */ static get styles(): CSSResultGroup { - return unsafeCSS(liveStyle); + return unsafeCSS(liveFrigateStyle); } } @@ -237,7 +279,7 @@ export class FrigateCardLiveWebRTC extends LitElement { * Get styles. */ static get styles(): CSSResultGroup { - return unsafeCSS(liveStyle); + return unsafeCSS(liveWebRTCStyle); } } @@ -415,6 +457,6 @@ export class FrigateCardLiveJSMPEG extends LitElement { * Get styles. */ static get styles(): CSSResultGroup { - return unsafeCSS(liveStyle); + return unsafeCSS(liveJSMPEGStyle); } } diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts index c7f43e08..a50c6f27 100644 --- a/src/components/thumbnail-carousel.ts +++ b/src/components/thumbnail-carousel.ts @@ -1,30 +1,102 @@ import { BrowseMediaUtil } from '../browse-media-util.js'; -import { CSSResultGroup, TemplateResult, html, unsafeCSS } from 'lit'; +import { CSSResultGroup, TemplateResult, html, unsafeCSS, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; +import { until } from 'lit/directives/until'; -import type { BrowseMediaSource, ThumbnailsControlConfig } from '../types.js'; -import { CarouselTap, FrigateCardCarousel } from './carousel.js'; +import type { BrowseMediaQueryParameters, BrowseMediaSource, ExtendedHomeAssistant, ThumbnailsControlConfig } from '../types.js'; +import { FrigateCardCarousel } from './carousel.js'; +import { HomeAssistant } from 'custom-card-helpers'; import { actionHandler } from '../action-handler-directive.js'; -import { dispatchFrigateCardEvent } from '../common.js'; +import { dispatchErrorMessageEvent, dispatchFrigateCardEvent } from '../common.js'; +import { renderProgressIndicator } from './message.js'; import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss'; +export interface ThumbnailCarouselTap { + slideIndex: number; + target: BrowseMediaSource; + childIndex: number; +} + @customElement('frigate-card-thumbnail-carousel') -export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { +export class FrigateCardThumbnailCarousel extends LitElement { + @property({ attribute: false }) + protected hass?: HomeAssistant & ExtendedHomeAssistant; + + @property({ attribute: false }) + protected browseMediaQueryParameters?: BrowseMediaQueryParameters; + + @property({ attribute: false }) + protected config?: ThumbnailsControlConfig; + + @property({ attribute: false }) + protected highlightSelected = true; + + /** + * Master render method. + * @returns A rendered template. + */ + protected render(): TemplateResult | void { + return html`${until(this._render(), renderProgressIndicator())}`; + } + + /** + * Asyncronously render the element. + * @returns A rendered template. + */ + protected async _render(): Promise { + if (!this.hass || !this.browseMediaQueryParameters) { + return html``; + } + + let parent: BrowseMediaSource | null = null; + try { + parent = await BrowseMediaUtil.browseMediaQuery( + this.hass, + this.browseMediaQueryParameters, + ); + } catch (e) { + return dispatchErrorMessageEvent(this, (e as Error).message); + } + return html` + `; + } + + /** + * Get element styles. + */ + // static get styles(): CSSResultGroup { + // return unsafeCSS(viewerStyle); + // } +} + +@customElement('frigate-card-thumbnail-carousel-core') +export class FrigateCardThumbnailCarouselCore extends FrigateCardCarousel { @property({ attribute: false }) protected target?: BrowseMediaSource; protected _tapSelected?; @property({ attribute: false }) - set config(config: ThumbnailsControlConfig) { - this._config = config; + set config(config: ThumbnailsControlConfig | undefined) { if (config) { - this.style.setProperty('--frigate-card-viewer-thumbnail-size', config.size); + if (config && (config.size !== undefined && config.size != null)) { + this.style.setProperty('--frigate-card-carousel-thumbnail-size', config.size); + } + this._config = config; } } protected _config?: ThumbnailsControlConfig; + @property({ attribute: false }) + set highlightSelected(value: boolean) { + this.style.setProperty('--frigate-card-carousel-thumbnail-opacity', value ? '0.6' : '1.0'); + } + constructor() { super(); this._options = { @@ -63,7 +135,10 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { const slides: TemplateResult[] = []; for (let i = 0; i < this.target.children.length; ++i) { - const thumbnail = this._renderThumbnail(this.target.children[i], slides.length); + const thumbnail = this._renderThumbnail( + this.target, + i, + slides.length); if (thumbnail) { slides.push(thumbnail); } @@ -77,9 +152,15 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { * @returns A template or void if the item could not be rendered. */ protected _renderThumbnail( - mediaToRender: BrowseMediaSource, + parent: BrowseMediaSource, + childIndex: number, slideIndex: number, ): TemplateResult | void { + if (!parent.children || !parent.children.length) { + return; + } + + const mediaToRender = parent.children[childIndex]; if (!BrowseMediaUtil.isTrueMedia(mediaToRender) || !mediaToRender.thumbnail) { return; } @@ -92,8 +173,10 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { })} @action=${() => { if (this._carousel && this._carousel.clickAllowed()) { - dispatchFrigateCardEvent(this, 'carousel:tap', { - index: slideIndex, + dispatchFrigateCardEvent(this, 'carousel:tap', { + slideIndex: slideIndex, + target: parent, + childIndex: childIndex, }); } }} diff --git a/src/components/viewer.ts b/src/components/viewer.ts index cd2fa7c8..89e2aa19 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -15,8 +15,8 @@ import type { MediaShowInfo, ViewerConfig, } from '../types.js'; -import { CarouselTap, FrigateCardCarousel } from './carousel.js'; -import { FrigateCardThumbnailCarousel } from './thumbnail-carousel.js'; +import { FrigateCardCarousel } from './carousel.js'; +import { FrigateCardThumbnailCarouselCore, ThumbnailCarouselTap } from './thumbnail-carousel.js'; import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js'; import { View } from '../view.js'; import { actionHandler } from '../action-handler-directive.js'; @@ -33,7 +33,6 @@ import { localize } from '../localize/localize.js'; import { renderProgressIndicator } from '../components/message.js'; import './next-prev-control.js'; -import './thumbnail-carousel.js'; import viewerStyle from '../scss/viewer.scss'; import viewerCoreStyle from '../scss/viewer-core.scss'; @@ -166,7 +165,7 @@ export class FrigateCardViewerCore extends LitElement { protected resolvedMediaCache?: ResolvedMediaCache; protected _mediaCarouselRef: Ref = createRef(); - protected _thumbnailCarouselRef: Ref = createRef(); + protected _thumbnailCarouselRef: Ref = createRef(); protected _syncThumbnailCarousel(): void { const mediaSelected = this._mediaCarouselRef.value?.carouselSelected(); @@ -180,16 +179,16 @@ export class FrigateCardViewerCore extends LitElement { return html``; } - return html` ) => { - this._mediaCarouselRef.value?.carouselScrollTo(ev.detail.index); + @frigate-card:carousel:tap=${(ev: CustomEvent) => { + this._mediaCarouselRef.value?.carouselScrollTo(ev.detail.slideIndex); }} @frigate-card:carousel:init=${this._syncThumbnailCarousel.bind(this)} > - `; + `; } protected render(): TemplateResult | void { diff --git a/src/scss/live-frigate.scss b/src/scss/live-frigate.scss new file mode 100644 index 00000000..a9b9a68b --- /dev/null +++ b/src/scss/live-frigate.scss @@ -0,0 +1,7 @@ +:host { + width: 100%; + height: 100%; + display: block; + + --video-max-height: none; +} \ No newline at end of file diff --git a/src/scss/live-jsmpeg.scss b/src/scss/live-jsmpeg.scss new file mode 100644 index 00000000..9f7cbf2c --- /dev/null +++ b/src/scss/live-jsmpeg.scss @@ -0,0 +1,12 @@ +:host { + width: 100%; + height: 100%; + display: block; +} + +canvas { + display: block; + width: 100%; + height: 100%; + object-fit: contain; +} \ No newline at end of file diff --git a/src/scss/live-webrtc.scss b/src/scss/live-webrtc.scss new file mode 100644 index 00000000..8297c2d8 --- /dev/null +++ b/src/scss/live-webrtc.scss @@ -0,0 +1,12 @@ +:host { + width: 100%; + height: 100%; + display: block; +} + +/* Don't drop shadow or have radius for nested webrtc card */ +webrtc-camera ha-card { + box-shadow: none; + border-radius: 0px; + background-color: var(--secondary-background-color, black); +} \ No newline at end of file diff --git a/src/scss/live.scss b/src/scss/live.scss index e1b6f47e..850a3197 100644 --- a/src/scss/live.scss +++ b/src/scss/live.scss @@ -1,21 +1,16 @@ :host { width: 100%; height: 100%; - display: block; - - --video-max-height: none; + display: flex; + flex-direction: column; + gap: 5px; } -canvas { - display: block; - width: 100%; - height: 100%; - object-fit: contain; +frigate-card-live { + flex: 1; + min-height: 0; } -/* Don't drop shadow or have radius for nested webrtc card */ -webrtc-camera ha-card { - box-shadow: none; - border-radius: 0px; - background-color: var(--secondary-background-color, black); +frigate-card-thumbnail-carousel { + flex: 0 0 var(--frigate-card-carousel-thumbnail-size); } \ No newline at end of file diff --git a/src/scss/thumbnail-carousel.scss b/src/scss/thumbnail-carousel.scss index 262b9c1a..1a1a417d 100644 --- a/src/scss/thumbnail-carousel.scss +++ b/src/scss/thumbnail-carousel.scss @@ -1,10 +1,11 @@ :host { - --frigate-card-viewer-thumbnail-size: 100px; + --frigate-card-carousel-thumbnail-size: 100px; + --frigate-card-carousel-thumbnail-opacity: 0.6; } .embla__slide { - flex: 0 0 var(--frigate-card-viewer-thumbnail-size); - opacity: 0.4; + flex: 0 0 var(--frigate-card-carousel-thumbnail-size); + opacity: var(--frigate-card-carousel-thumbnail-opacity); transition: opacity 0.6s ease, transform 0.3s ease; } .embla__slide.slide-selected { @@ -15,4 +16,9 @@ } .embla__slide img { border-radius: 5px; + + // Not 'contain' as some thumbnails may vary in aspect-ratio slightly and + // should be clipped to fill the thumbnail div whilst maintaining + // aspect-ratio. + object-fit: cover; } \ No newline at end of file diff --git a/src/scss/viewer-core.scss b/src/scss/viewer-core.scss index b3539631..7dcf4a98 100644 --- a/src/scss/viewer-core.scss +++ b/src/scss/viewer-core.scss @@ -1,6 +1,7 @@ :host { display: flex; flex-direction: column; + gap: 5px; height: 100%; width: 100%; @@ -11,7 +12,6 @@ frigate-card-media-carousel { min-height: 0; } -frigate-card-thumbnail-carousel { - flex: 0 0 var(--frigate-card-viewer-thumbnail-size); - margin-top: 5px; +frigate-card-thumbnail-carousel-core { + flex: 0 0 var(--frigate-card-carousel-thumbnail-size); } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index f94e79c2..73abcb01 100644 --- a/src/types.ts +++ b/src/types.ts @@ -355,12 +355,29 @@ const imageConfigSchema = z .optional(); export type ImageViewConfig = z.infer; +/** + * Thumbnail controls configuration section. + */ + +// This type is not inferred from a schema since different (compatible) schemas +// have different defaults. +export type ThumbnailsControlConfig = { + mode: 'above' | 'below' | 'none'; + size?: string; +} + /** * Live view configuration section. */ const liveConfigDefault = { provider: 'frigate' as const, preload: false, + controls: { + thumbnails: { + mode: 'none' as const, + media: 'clips' as const, + }, + }, }; const webrtcConfigSchema = z .object({ @@ -400,6 +417,19 @@ const liveConfigSchema = z preload: z.boolean().default(liveConfigDefault.preload), webrtc: webrtcConfigSchema, jsmpeg: jsmpegConfigSchema, + controls: z + .object({ + thumbnails: z + .object({ + media: z + .enum(['clips', 'snapshots']) + .default(liveConfigDefault.controls.thumbnails.media), + mode: z.enum(['none', 'above', 'below']).default(liveConfigDefault.controls.thumbnails.mode), + size: z.string().optional(), + }) + .default(liveConfigDefault.controls.thumbnails), + }) + .default(liveConfigDefault.controls), }) .merge(actionsSchema) .default(liveConfigDefault); @@ -455,7 +485,6 @@ const viewerConfigDefault = { style: 'thumbnails' as const, }, thumbnails: { - size: '100px', mode: 'below' as const, }, }, @@ -470,16 +499,6 @@ const nextPreviousControlConfigSchema = z .default(viewerConfigDefault.controls.next_previous); export type NextPreviousControlConfig = z.infer; -const thumbnailsControlConfigSchema = z - .object({ - mode: z - .enum(['none', 'above', 'below']) - .default(viewerConfigDefault.controls.thumbnails.mode), - size: z.string().default(viewerConfigDefault.controls.thumbnails.size), - }) - .default(viewerConfigDefault.controls.thumbnails); -export type ThumbnailsControlConfig = z.infer; - const viewerConfigSchema = z .object({ autoplay_clip: z.boolean().default(viewerConfigDefault.autoplay_clip), @@ -488,7 +507,10 @@ const viewerConfigSchema = z controls: z .object({ next_previous: nextPreviousControlConfigSchema, - thumbnails: thumbnailsControlConfigSchema, + thumbnails: z.object({ + mode: z.enum(['none', 'above', 'below']).default(viewerConfigDefault.controls.thumbnails.mode), + size: z.string().optional(), + }).default(viewerConfigDefault.controls.thumbnails), }) .default(viewerConfigDefault.controls), }) From b0bcc6eda58e0503473a0efb6885c0bc5a298f28 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Thu, 25 Nov 2021 20:38:24 -0800 Subject: [PATCH 08/13] Sync thumbnail styles between carousel and gallery. --- src/components/gallery.ts | 4 ++-- src/scss/gallery.scss | 13 ++++++++++--- src/scss/thumbnail-carousel.scss | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/gallery.ts b/src/components/gallery.ts index e4751776..a1f60247 100644 --- a/src/components/gallery.ts +++ b/src/components/gallery.ts @@ -146,8 +146,8 @@ export class FrigateCardGalleryCore extends LitElement { } const styles = { - // Controls the number of columns in the gallery (allows for 1px gutter). - width: `calc(${100 / this._columns}% - 1.2px)`, + // Controls the number of columns in the gallery (allows for 5px gutter). + width: `calc(${100 / this._columns}% - 5.25px)`, }; return html`