From 6c03934656e2c17c47ee667c091abfea5b870e9f Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Mon, 22 Nov 2021 17:07:33 -0800 Subject: [PATCH] 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