diff --git a/src/browse-media-util.ts b/src/browse-media-util.ts index 37f19352..1fbfe1d6 100644 --- a/src/browse-media-util.ts +++ b/src/browse-media-util.ts @@ -116,9 +116,9 @@ export class BrowseMediaUtil { static getBrowseMediaQueryParameters( mediaType: 'clips' | 'snapshots', cameraConfig?: CameraConfig, - ): BrowseMediaQueryParameters | undefined { + ): BrowseMediaQueryParameters | null { if (!cameraConfig || !cameraConfig.camera_name) { - return undefined; + return null; } return { mediaType: mediaType, @@ -137,9 +137,9 @@ export class BrowseMediaUtil { node: HTMLElement, view: View, cameraConfig: CameraConfig, - ): BrowseMediaQueryParameters | undefined { + ): BrowseMediaQueryParameters | null { if (!view.isClipRelatedView() && !view.isSnapshotRelatedView()) { - return undefined; + return null; } // Verify there is a camera name, otherwise getBrowseMediaQueryParameters() @@ -149,7 +149,7 @@ export class BrowseMediaUtil { node, localize('error.no_camera_name') + `: ${JSON.stringify(cameraConfig)}`, ); - return undefined; + return null; } return BrowseMediaUtil.getBrowseMediaQueryParameters( diff --git a/src/components/carousel.ts b/src/components/carousel.ts index a3bd237a..9cd9b923 100644 --- a/src/components/carousel.ts +++ b/src/components/carousel.ts @@ -6,7 +6,7 @@ import EmblaCarousel, { EmblaOptionsType, EmblaPluginType, } from 'embla-carousel'; -import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures' +import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures'; import { TransitionEffect } from '../types'; import { dispatchFrigateCardEvent } from '../common'; @@ -78,11 +78,13 @@ export class FrigateCardCarousel extends LitElement { * @returns An EmblaOptionsType object or undefined for no options. */ protected _getPlugins(): EmblaPluginType[] { - return [WheelGesturesPlugin({ - // Whether the carousel is vertical or horizontal, interpret y-axis wheel - // gestures as scrolling for the carousel. - forceWheelAxis: 'y', - })]; + return [ + WheelGesturesPlugin({ + // Whether the carousel is vertical or horizontal, interpret y-axis wheel + // gestures as scrolling for the carousel. + forceWheelAxis: 'y', + }), + ]; } protected _destroyCarousel(): void { @@ -112,9 +114,10 @@ export class FrigateCardCarousel extends LitElement { carouselNode, { axis: this.direction == 'horizontal' ? 'x' : 'y', - ...this._getOptions() + ...this._getOptions(), }, - plugins); + plugins, + ); this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init')); this._carousel.on('select', () => { const selected = this.carouselSelected(); diff --git a/src/components/live.ts b/src/components/live.ts index cccd9a6d..62be17ff 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -120,79 +120,12 @@ export class FrigateCardLive extends LitElement { } } - /** - * Render thumbnails carousel. - * @returns A rendered template or void. - */ - protected renderThumbnails(config: LiveConfig): TemplateResult | void { - if (!this.liveConfig || !this.view) { - return; - } - - const fetchThumbnailsThenRender = async (): Promise => { - if (!this.hass || !this.cameras || !this.view) { - return; - } - const browseMediaParams = BrowseMediaUtil.getBrowseMediaQueryParameters( - config.controls.thumbnails.media, - this.cameras.get(this.view.camera), - ); - if (!browseMediaParams) { - return; - } - let parent: FrigateBrowseMediaSource | null; - try { - parent = await BrowseMediaUtil.browseMediaQuery(this.hass, browseMediaParams); - } catch (e) { - return dispatchErrorMessageEvent(this, (e as Error).message); - } - - if (BrowseMediaUtil.getFirstTrueMediaChildIndex(parent) != null) { - return html`) => { - const mediaType = browseMediaParams.mediaType; - if (mediaType && this.view && ['snapshots', 'clips'].includes(mediaType)) { - new View({ - view: mediaType === 'clips' ? 'clip' : 'snapshot', - camera: this.view.camera, - target: ev.detail.target, - childIndex: ev.detail.childIndex, - }).dispatchChangeEvent(this); - } - }} - > - `; - } - }; - - const fillerStyle = { - height: config.controls.thumbnails.size, - }; - - // As the live carousel moves, thumbnails are re-fetched. This is an async - // request, so it can jarring to the user to have the main camera view nudge - // up/down as the thumbnails disappear and re-appear. Instead, if there was - // previously a thumbnail carousel rendered, use a filler that is the same - // size until it is replaced with a real carousel (or empty, if no carousel - // is rendered for the next camera). - return html`${until( - fetchThumbnailsThenRender(), - this._thumbnailCarousel - ? html`
` - : html``, - )}`; - } - /** * Master render method. * @returns A rendered template. */ protected render(): TemplateResult | void { - if (!this.hass || !this.liveConfig || !this.cameras) { + if (!this.hass || !this.liveConfig || !this.cameras || !this.view) { return; } @@ -202,11 +135,24 @@ export class FrigateCardLive extends LitElement { this.conditionState, ) as LiveConfig; + const browseMediaParams = BrowseMediaUtil.getBrowseMediaQueryParameters( + config.controls.thumbnails.media, + this.cameras.get(this.view.camera), + ); + if (!browseMediaParams) { + return; + } + // Note use of liveConfig and not config below -- the carousel will // independently override the liveconfig to reflect the camera in the // carousel (not necessarily the selected camera). - return html` - ${config.controls.thumbnails.mode === 'above' ? this.renderThumbnails(config) : ''} + return html` - ${config.controls.thumbnails.mode === 'below' ? this.renderThumbnails(config) : ''} - `; + `; } /** diff --git a/src/components/surround-thumbnails.ts b/src/components/surround-thumbnails.ts index 3283a62b..ca375539 100644 --- a/src/components/surround-thumbnails.ts +++ b/src/components/surround-thumbnails.ts @@ -1,20 +1,20 @@ import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; import { HomeAssistant } from 'custom-card-helpers'; -import { createRef, ref, Ref } from 'lit/directives/ref.js'; -import { customElement, property } from 'lit/decorators.js'; +import { Task } from '@lit-labs/task'; +import { customElement, property, state } from 'lit/decorators.js'; +import { BrowseMediaUtil } from '../browse-media-util.js'; import { + BrowseMediaQueryParameters, ExtendedHomeAssistant, FrigateBrowseMediaSource, FrigateCardView, ThumbnailsControlConfig, } from '../types.js'; -import { - FrigateCardThumbnailCarousel, - ThumbnailCarouselTap, -} from './thumbnail-carousel.js'; + +import { ThumbnailCarouselTap } from './thumbnail-carousel.js'; import { View } from '../view.js'; -import { dispatchFrigateCardEvent } from '../common.js'; +import { dispatchErrorMessageEvent, dispatchFrigateCardEvent } from '../common.js'; import './surround.js'; @@ -39,7 +39,48 @@ export class FrigateCardSurround extends LitElement { @property({ attribute: false }) protected targetView?: FrigateCardView; - protected _refThumbnails: Ref = createRef(); + @property({ attribute: false }) + protected browseMediaParams?: BrowseMediaQueryParameters; + + @state() + protected _thumbnailTarget?: FrigateBrowseMediaSource; + + @state() + protected _thumbnailSelected?: number | null; + + // A task to await the load of the WebRTC component. + protected _browseTask = new Task(this, this._fetchMedia.bind(this), () => [ + this.hass, + this.browseMediaParams, + ]); + + /** + * Fetch thumbnail media. + * @param param Task parameters. + * @returns + */ + protected async _fetchMedia([hass, browseMediaParams]: ( + | (HomeAssistant & ExtendedHomeAssistant) + | BrowseMediaQueryParameters + | undefined + )[]): Promise { + hass = hass as HomeAssistant & ExtendedHomeAssistant; + browseMediaParams = browseMediaParams as BrowseMediaQueryParameters; + + if (!hass || !browseMediaParams) { + return; + } + let parent: FrigateBrowseMediaSource | null; + try { + parent = await BrowseMediaUtil.browseMediaQuery(hass, browseMediaParams); + } catch (e) { + return dispatchErrorMessageEvent(this, (e as Error).message); + } + if (BrowseMediaUtil.getFirstTrueMediaChildIndex(parent) != null) { + this._thumbnailTarget = parent; + this._thumbnailSelected = null; + } + } /** * Master render method. @@ -52,10 +93,8 @@ export class FrigateCardSurround extends LitElement { return html` ) => { - if (this._refThumbnails.value) { - this._refThumbnails.value.target = ev.detail.target; - this._refThumbnails.value.selected = ev.detail.childIndex ?? undefined; - } + this._thumbnailTarget = ev.detail.target; + this._thumbnailSelected = ev.detail.childIndex; }} @frigate-card:thumbnails:open=${(ev: CustomEvent) => { if (this.config && ['left', 'right'].includes(this.config.mode)) { @@ -73,19 +112,17 @@ export class FrigateCardSurround extends LitElement { ${this.config?.mode !== 'none' ? html` ) => { - if (ev.detail.target && ev.detail.childIndex) { - this.view - ?.evolve({ - ...(this.targetView && { view: this.targetView }), - target: ev.detail.target, - childIndex: ev.detail.childIndex, - }) - .dispatchChangeEvent(this); - } + this.view + ?.evolve({ + ...(this.targetView && { view: this.targetView }), + target: ev.detail.target, + childIndex: ev.detail.childIndex, + }) + .dispatchChangeEvent(this); }} > ` diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts index 35bcfc2a..4fa71dc4 100644 --- a/src/components/thumbnail-carousel.ts +++ b/src/components/thumbnail-carousel.ts @@ -13,7 +13,7 @@ import { stopEventFromActivatingCardWideActions, } from '../common.js'; -import "./thumbnail.js"; +import './thumbnail.js'; import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss'; @@ -28,8 +28,24 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { @property({ attribute: false, hasChanged: contentsChanged }) public target?: FrigateBrowseMediaSource; - @property({ attribute: false, reflect: true }) - public selected?: number | null; + // Thumbnail carousels can expand (e.g. drawer-based carousels after the main + // media loads). The carousel must be re-initialized in these cases, or the + // dynamic sizing fails (and users can scroll past the end of the carousel). + protected _resizeObserver: ResizeObserver; + + constructor() { + super(); + this._resizeObserver = new ResizeObserver(this._resizeHandler.bind(this)); + } + + @property({ attribute: false }) + set selected(selected: number | null) { + this._selected = selected; + if (selected !== null) { + // If there is a selection, 'dim' all the other slides. + this.style.setProperty('--frigate-card-carousel-thumbnail-opacity', '0.4'); + } + } @property({ attribute: false }) set config(config: ThumbnailsControlConfig) { @@ -40,12 +56,32 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { @state() protected _config?: ThumbnailsControlConfig; - @property({ attribute: false }) - set highlight_selected(value: boolean) { - this.style.setProperty( - '--frigate-card-carousel-thumbnail-opacity', - value ? '0.4' : '1.0', - ); + @state() + protected _selected?: number | null; + + /** + * Handle gallery resize. + */ + protected _resizeHandler(): void { + if (this._carousel) { + this._carousel.reInit(); + } + } + + /** + * Component connected callback. + */ + connectedCallback(): void { + super.connectedCallback(); + this._resizeObserver.observe(this); + } + + /** + * Component disconnected callback. + */ + disconnectedCallback(): void { + this._resizeObserver.disconnect(); + super.disconnectedCallback(); } /** @@ -120,27 +156,26 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { const classes = { embla__slide: true, - 'slide-selected': this.selected == childIndex, + 'slide-selected': this.selected === childIndex, }; - return html` - { - if (this._carousel && this._carousel.clickAllowed()) { - dispatchFrigateCardEvent(this, 'carousel:tap', { - slideIndex: slideIndex, - target: parent, - childIndex: childIndex, - }); - } - stopEventFromActivatingCardWideActions(ev); - }} - > - `; + return html` { + if (this._carousel && this._carousel.clickAllowed()) { + dispatchFrigateCardEvent(this, 'carousel:tap', { + slideIndex: slideIndex, + target: parent, + childIndex: childIndex, + }); + } + stopEventFromActivatingCardWideActions(ev); + }} + > + `; } /** diff --git a/src/scss/carousel.scss b/src/scss/carousel.scss index 46c973c2..32dc9147 100644 --- a/src/scss/carousel.scss +++ b/src/scss/carousel.scss @@ -23,17 +23,15 @@ img,video { width: 100%; height: 100%; - flex-direction: column; - user-select: none; -webkit-touch-callout: none; -khtml-user-select: none; -webkit-tap-highlight-color: transparent; } -:host([direction="vertical"]) .embla__container { +:host([direction=vertical]) .embla__container { flex-direction: column; } -:host([direction="horizontal"]) .embla__container { +:host([direction=horizontal]) .embla__container { flex-direction: row; } @@ -60,10 +58,10 @@ img,video { height: 100%; overflow: visible; } -:host([direction="vertical"]) .embla__slide { +:host([direction=vertical]) .embla__slide { margin-bottom: 5px; } -:host([direction="horizontal"]) .embla__slide { +:host([direction=horizontal]) .embla__slide { margin-right: 5px; } .embla__slide img,video { diff --git a/src/scss/drawer.scss b/src/scss/drawer.scss index 3743303e..96cda137 100644 --- a/src/scss/drawer.scss +++ b/src/scss/drawer.scss @@ -8,6 +8,7 @@ side-drawer { div.control-surround { position: absolute; bottom: 50%; + transform: translateY(50%); z-index: 0; padding-top: $drawer-padding-extend; padding-bottom: $drawer-padding-extend; @@ -29,19 +30,20 @@ div.control-surround { ha-icon.control { color: var(--secondary-color, white); - background-color: rgba(0, 0, 0, 0.6); - opacity: 0.6; + background-color: rgba(0, 0, 0, 0.7); + opacity: 0.7; pointer-events: all; --mdc-icon-size: #{$drawer-icon-size}; padding-top: $drawer-padding-extend; padding-bottom: $drawer-padding-extend; - transition: opacity 1s ease; + transition: opacity 0.5s ease; } -:host([open]) ha-icon.control { - // When the drawer is open make the button to close it more prominent. +:host([open]) ha-icon.control, ha-icon.control:hover { + // When the drawer is open or hovered make the button to close it more + // prominent. opacity: 1; background-color: black; } diff --git a/src/scss/surround.scss b/src/scss/surround.scss index 47236da5..173850ba 100644 --- a/src/scss/surround.scss +++ b/src/scss/surround.scss @@ -6,7 +6,7 @@ display: flex; flex-direction: column; - // So the drawer is relative to this host. + // Set the drawer relative to this host. position: relative; } diff --git a/src/scss/thumbnail-carousel.scss b/src/scss/thumbnail-carousel.scss index 07d0eacb..bf20b658 100644 --- a/src/scss/thumbnail-carousel.scss +++ b/src/scss/thumbnail-carousel.scss @@ -1,5 +1,5 @@ :host { - --frigate-card-carousel-thumbnail-opacity: 0.8; + --frigate-card-carousel-thumbnail-opacity: 1.0; } :host([direction=vertical]) {