${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');
+ }}
+ >`
+ : ``}
+ ${neighbors && neighbors.next
+ ? html`
{
+ this._nextPreviousHandler('next');
+ }}
+ >`
+ : ``}
- ${neighbors && neighbors.next
- ? html`
{
- this._nextPreviousHandler('next');
- }}
- >`
- : ``}
-
`;
+
`;
}
/**
@@ -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