From bb36004b6d0e0612e69ffddf906396af446b483b Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Wed, 24 Nov 2021 14:42:41 -0800 Subject: [PATCH] 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), })