diff --git a/src/components/media-carousel.ts b/src/components/media-carousel.ts index 8f7c7280..810d072d 100644 --- a/src/components/media-carousel.ts +++ b/src/components/media-carousel.ts @@ -22,6 +22,7 @@ import './next-prev-control.js'; import './carousel.js'; import { FrigateCardNextPreviousControl } from './next-prev-control.js'; import { FrigateCardTitleControl } from './title-control.js'; +import { debounce } from 'lodash-es'; 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`; @@ -143,10 +144,14 @@ export class FrigateCardMediaCarousel extends LitElement { protected _boundAutoPlayHandler = this.autoPlay.bind(this); protected _boundAutoUnmuteHandler = this.autoUnmute.bind(this); - protected _boundAdaptContainerHeightToSlide = - this._adaptContainerHeightToSlide.bind(this); protected _boundTitleHandler = this._titleHandler.bind(this); + // Debounce multiple calls to adapt the container height. + protected _debouncedAdaptContainerHeightToSlide = debounce( + this._adaptContainerHeightToSlide.bind(this), + 1 * 100, + {trailing: true}); + // This carousel may be resized by Lovelace resizes, window resizes, // fullscreen, etc. Always call the adaptive height handler when the size // changes. @@ -272,10 +277,9 @@ export class FrigateCardMediaCarousel extends LitElement { this.addEventListener('frigate-card:media:loaded', this._boundAutoUnmuteHandler); this.addEventListener( 'frigate-card:media:loaded', - this._boundAdaptContainerHeightToSlide, + this._debouncedAdaptContainerHeightToSlide, ); this.addEventListener('frigate-card:media:loaded', this._boundTitleHandler); - this._resizeObserver.observe(this); this._intersectionObserver.observe(this); } @@ -287,7 +291,7 @@ export class FrigateCardMediaCarousel extends LitElement { this.removeEventListener('frigate-card:media:loaded', this._boundAutoUnmuteHandler); this.removeEventListener( 'frigate-card:media:loaded', - this._boundAdaptContainerHeightToSlide, + this._debouncedAdaptContainerHeightToSlide, ); this.removeEventListener('frigate-card:media:loaded', this._boundTitleHandler); this._resizeObserver.disconnect(); @@ -302,7 +306,7 @@ export class FrigateCardMediaCarousel extends LitElement { */ protected _reInitAndAdjustHeight(): void { this.frigateCardCarousel()?.carouselReInitWhenSafe(); - this._adaptContainerHeightToSlide(); + this._debouncedAdaptContainerHeightToSlide(); } /** @@ -331,36 +335,25 @@ export class FrigateCardMediaCarousel extends LitElement { * actually the media load/show that will change the dimensions, and that is * async from carousel actions (e.g. lazy-loaded media). * - * This component does not use the stock Embla auto-height plugin as it - * resizes the container on selection rather than media load. + * This component does not use the stock Embla auto-height plugin as that + * resizes the container only on selection rather than media load. */ protected _adaptContainerHeightToSlide(): void { - const adaptCarouselHeight = (): void => { - const selected = this.frigateCardCarousel()?.getCarouselSelected(); - if (selected) { - this.style.removeProperty('max-height'); - const height = selected.element.getBoundingClientRect().height; - if (height !== undefined && height > 0) { - this.style.maxHeight = `${height}px`; - } + const selected = this.frigateCardCarousel()?.getCarouselSelected(); + if (selected) { + this.style.removeProperty('max-height'); + const height = selected.element.getBoundingClientRect().height; + if (height !== undefined && height > 0) { + this.style.maxHeight = `${height}px`; } - }; - - // Hack: This method attempts to measure the height of the selected slide in - // order to set the overall carousel height to match. This method is - // triggered from `frigate-card:media:loaded` events, which are usually in - // turn triggered from media/metadata load events from media players. - // Sufficient time needs to be allowed after these metadata load events to - // allow the browser to repaint the element heights, so that we can get the - // right values here. requestAnimationFrame() works well for this. - window.requestAnimationFrame(adaptCarouselHeight); + } } /** * Fire a media show event when a slide is selected. */ - protected _dispatchMediaLoadedInfo(): void { - const slideIndex = this.frigateCardCarousel()?.getCarouselSelected()?.index; + protected _dispatchMediaLoadedInfo(selected: CarouselSelect): void { + const slideIndex = selected.index; if (slideIndex !== undefined && slideIndex in this._mediaLoadedInfo) { dispatchExistingMediaLoadedInfoAsEvent(this, this._mediaLoadedInfo[slideIndex]); } @@ -404,26 +397,40 @@ export class FrigateCardMediaCarousel extends LitElement { } protected render(): TemplateResult | void { + const selectSlide = (ev?: CustomEvent): void => { + this._slideResizeObserver.disconnect(); + const parent = this.getRootNode(); + if (parent && parent instanceof ShadowRoot) { + this._slideResizeObserver.observe(parent.host); + } + + const selected = ev ? ev.detail : this.frigateCardCarousel()?.getCarouselSelected(); + if (selected) { + this._slideResizeObserver.observe(selected.element); + + // Pass up the media-carousel select event first to allow parents to + // initialize/reset before the media info is dispatched. + dispatchFrigateCardEvent( + this, + 'media-carousel:select', + selected, + ); + + // Dispatch media info. + this._dispatchMediaLoadedInfo(selected); + } + } + return html` { + selectSlide(); + }} @frigate-card:carousel:select=${(ev: CustomEvent) => { - this._slideResizeObserver.disconnect(); - this._slideResizeObserver.observe(ev.detail.element); - - // Pass up the media-carousel select event first to allow parents to - // initialize/reset before the media info is dispatched. - dispatchFrigateCardEvent( - this, - 'media-carousel:select', - ev.detail, - ); - - // Dispatch media info. - this._dispatchMediaLoadedInfo(); + selectSlide(ev); }} @frigate-card:carousel:media:loaded=${this._storeMediaLoadedInfo.bind(this)} @frigate-card:carousel:media:unloaded=${this._removeMediaLoadedInfo.bind(this)} diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts index 35778224..3d76e00e 100644 --- a/src/components/timeline-core.ts +++ b/src/components/timeline-core.ts @@ -328,7 +328,9 @@ export class FrigateCardTimelineCore extends LitElement { if (!onlyShowMatchingHour || isMatchingHour) { children.push( createChild( - `${prettifyTitle(config.frigate.camera_name)} ${formatDateAndTime(hour)}`, + `${prettifyTitle(config.frigate.camera_name)} ${formatDateAndTime( + hour, + )}`, getRecordingMediaContentID({ clientId: config.frigate.client_id, year: dayData.day.getFullYear(), @@ -466,7 +468,6 @@ export class FrigateCardTimelineCore extends LitElement { properties.event.additionalEvent !== 'pinchin' && properties.event.additionalEvent !== 'pinchout' ) { - const targetTime = this._pointerHeld?.window ? add(properties.start, { seconds: @@ -683,7 +684,8 @@ export class FrigateCardTimelineCore extends LitElement { if (properties.group) { this._changeViewToRecording( properties.what === 'background' ? properties.time : window.end, - String(properties.group)); + String(properties.group), + ); } else if (this.mini && this.view?.camera) { // In mini mode group may not be displayed / used, so just use the camera directly. this._changeViewToRecording(window.end, this.view.camera); @@ -816,48 +818,50 @@ export class FrigateCardTimelineCore extends LitElement { : this._timeline.getSelection(); let childIndex = -1; const children: FrigateBrowseMediaSource[] = []; - this._dataview?.get({ - filter: (item) => item.type !== 'background', - order: sortTimelineItemsYoungestToOldest } - ).forEach((item) => { - const cameraID = item.group ? String(item.group) : null; - const cameraConfig = cameraID ? this.cameras?.get(cameraID) : null; - const event = item.event; - const media = - event?.has_clip && this.timelineConfig?.media !== 'snapshots' - ? 'clips' - : event?.has_snapshot - ? 'snapshots' - : null; + this._dataview + ?.get({ + filter: (item) => item.type !== 'background', + order: sortTimelineItemsYoungestToOldest, + }) + .forEach((item) => { + const cameraID = item.group ? String(item.group) : null; + const cameraConfig = cameraID ? this.cameras?.get(cameraID) : null; + const event = item.event; + const media = + event?.has_clip && this.timelineConfig?.media !== 'snapshots' + ? 'clips' + : event?.has_snapshot + ? 'snapshots' + : null; - if ( - cameraID && - cameraConfig && - event && - media && - cameraConfig.frigate.camera_name - ) { - children.push( - createChild( - getEventTitle(event), - getEventMediaContentID( - cameraConfig.frigate.client_id, - cameraConfig.frigate.camera_name, - event.id, - media, + if ( + cameraID && + cameraConfig && + event && + media && + cameraConfig.frigate.camera_name + ) { + children.push( + createChild( + getEventTitle(event), + getEventMediaContentID( + cameraConfig.frigate.client_id, + cameraConfig.frigate.camera_name, + event.id, + media, + ), + { + thumbnail: getEventThumbnailURL(cameraConfig.frigate.client_id, event), + event: event, + cameraID: cameraID, + }, ), - { - thumbnail: getEventThumbnailURL(cameraConfig.frigate.client_id, event), - event: event, - cameraID: cameraID, - }, - ), - ); - if (selected.includes(event.id)) { - childIndex = children.length - 1; + ); + if (selected.includes(event.id)) { + childIndex = children.length - 1; + } } - } - }); + }); if (!children.length) { return null; } @@ -1053,13 +1057,7 @@ export class FrigateCardTimelineCore extends LitElement { * Update the timeline from the view object. */ protected async _updateTimelineFromView(): Promise { - if ( - !this.hass || - !this.cameras || - !this.view || - !this.timelineConfig || - !this._timeline - ) { + if (!this.hass || !this.cameras || !this.view || !this.timelineConfig) { return; } @@ -1091,7 +1089,7 @@ export class FrigateCardTimelineCore extends LitElement { )); } - this._timeline.setSelection(event ? [event.id] : [], { + this._timeline?.setSelection(event ? [event.id] : [], { focus: false, animation: { animation: false, @@ -1107,7 +1105,11 @@ export class FrigateCardTimelineCore extends LitElement { this.timelineDataManager?.rewriteItem(event.id); } - if (!this._pointerHeld && !this.view.context?.timeline?.noSetWindow) { + if ( + !this._pointerHeld && + !this.view.context?.timeline?.noSetWindow && + this._timeline + ) { // Regenerate the thumbnails after the selection, to allow the new selection // to be in the generated view. const context = this.view.context?.timeline; diff --git a/src/editor.ts b/src/editor.ts index e9e56710..76b45c52 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -294,7 +294,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor value: 'icons', label: localize('config.common.controls.next_previous.styles.icons'), }, - { value: 'none', label: localize('config.common.controls.next_previous.styles.none') }, + { + value: 'none', + label: localize('config.common.controls.next_previous.styles.none'), + }, ]; protected _aspectRatioModes: EditorSelectOption[] = [ @@ -336,7 +339,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor protected _thumbnailMedias: EditorSelectOption[] = [ { value: '', label: '' }, - { value: 'clips', label: localize('config.common.controls.thumbnails.medias.clips') }, + { + value: 'clips', + label: localize('config.common.controls.thumbnails.medias.clips'), + }, { value: 'snapshots', label: localize('config.common.controls.thumbnails.medias.snapshots'), @@ -1388,7 +1394,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${this._expandedMenus[MENU_OPTIONS] === 'cameras' ? html`
- ${cameras.map((_, index) => this._renderCamera(cameras, index, entities))} + ${cameras.map((_, index) => + this._renderCamera(cameras, index, entities), + )} ${this._renderCamera(cameras, cameras.length, entities, true)}
` @@ -1424,18 +1432,18 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${this._renderNumberInput(CONF_MENU_BUTTON_SIZE, { min: BUTTON_SIZE_MIN, })} + ${this._renderMenuButton('frigate') /* */} + ${this._renderMenuButton('cameras') /* */} + ${this._renderMenuButton('live') /* */} + ${this._renderMenuButton('clips') /* */} + ${this._renderMenuButton('snapshots')} + ${this._renderMenuButton('image') /* */} + ${this._renderMenuButton('download')} + ${this._renderMenuButton('frigate_ui')} + ${this._renderMenuButton('fullscreen')} + ${this._renderMenuButton('timeline')} + ${this._renderMenuButton('media_player')} - ${this._renderMenuButton('frigate') /* */} - ${this._renderMenuButton('cameras') /* */} - ${this._renderMenuButton('live') /* */} - ${this._renderMenuButton('clips') /* */} - ${this._renderMenuButton('snapshots')} - ${this._renderMenuButton('image') /* */} - ${this._renderMenuButton('download')} - ${this._renderMenuButton('frigate_ui')} - ${this._renderMenuButton('fullscreen')} - ${this._renderMenuButton('timeline')} - ${this._renderMenuButton('media_player')} ` : ''} ${this._renderOptionSetHeader('live')} @@ -1493,7 +1501,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor { configPathMedia: CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA, configPathMode: CONF_LIVE_CONTROLS_THUMBNAILS_MODE, - } + }, )} ${this._renderTitleControls( MENU_LIVE_CONTROLS_TITLE, @@ -1582,7 +1590,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL, { configPathMode: CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE, - } + }, )} ${this._renderTitleControls( MENU_MEDIA_VIEWER_CONTROLS_TITLE, @@ -1641,7 +1649,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL, { configPathMode: CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE, - } + }, )} ` : ''} diff --git a/src/scss/live-webrtc.scss b/src/scss/live-webrtc.scss index 6482aed1..492e8e62 100644 --- a/src/scss/live-webrtc.scss +++ b/src/scss/live-webrtc.scss @@ -9,13 +9,19 @@ /* Don't drop shadow or have radius for nested webrtc card */ #webrtc ha-card { border-radius: 0px; - background-color: unset; // To get the WebRTC player to line up correctly with some themes. margin: 0px; box-shadow: none; } +ha-card, +div.fix-safari, +#video { + background: unset; + background-color: unset; +} + #webrtc #video { @include media-layout.media-layout(); }