Ensure container resizes when parent resizes.

This commit is contained in:
Dermot Duffy
2022-10-01 23:49:35 -07:00
parent b4879b53df
commit 3c247b6320
4 changed files with 134 additions and 111 deletions
+49 -42
View File
@@ -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<CarouselSelect>): 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<CarouselSelect>(
this,
'media-carousel:select',
selected,
);
// Dispatch media info.
this._dispatchMediaLoadedInfo(selected);
}
}
return html` <frigate-card-carousel
${ref(this._refCarousel)}
.carouselOptions=${this.carouselOptions}
.carouselPlugins=${this.carouselPlugins}
transitionEffect=${ifDefined(this.transitionEffect)}
@frigate-card:carousel:init=${this._dispatchMediaLoadedInfo.bind(this)}
@frigate-card:carousel:init=${() => {
selectSlide();
}}
@frigate-card:carousel:select=${(ev: CustomEvent<CarouselSelect>) => {
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<CarouselSelect>(
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)}
+53 -51
View File
@@ -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<void> {
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;
+25 -17
View File
@@ -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`
<div class="values">
${cameras.map((_, index) => this._renderCamera(cameras, index, entities))}
${cameras.map((_, index) =>
this._renderCamera(cameras, index, entities),
)}
${this._renderCamera(cameras, cameras.length, entities, true)}
</div>
`
@@ -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')}
</div>
${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,
}
},
)}
</div>`
: ''}
+7 -1
View File
@@ -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();
}