+10
@@ -129,6 +129,15 @@ class AdvancedCameraCard extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set isPanel(isPanel: boolean) {
|
||||||
|
this._controller.getConditionStateManager().setState({
|
||||||
|
panel: isPanel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
get isPanel(): boolean {
|
||||||
|
return !!this._controller.getConditionStateManager().getState().panel;
|
||||||
|
}
|
||||||
|
|
||||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||||
return await CardController.getConfigElement();
|
return await CardController.getConfigElement();
|
||||||
}
|
}
|
||||||
@@ -380,6 +389,7 @@ class AdvancedCameraCard extends LitElement {
|
|||||||
.configManager=${this._controller.getConfigManager()}
|
.configManager=${this._controller.getConfigManager()}
|
||||||
.hide=${!!this._controller.getMessageManager().hasMessage()}
|
.hide=${!!this._controller.getMessageManager().hasMessage()}
|
||||||
.microphoneState=${this._controller.getMicrophoneManager().getState()}
|
.microphoneState=${this._controller.getMicrophoneManager().getState()}
|
||||||
|
.conditionStateManager=${this._controller.getConditionStateManager()}
|
||||||
.triggeredCameraIDs=${this._config?.view.triggers.show_trigger_status
|
.triggeredCameraIDs=${this._config?.view.triggers.show_trigger_status
|
||||||
? this._controller.getTriggersManager().getTriggeredCameraIDs()
|
? this._controller.getTriggersManager().getTriggeredCameraIDs()
|
||||||
: undefined}
|
: undefined}
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { ReactiveController, ReactiveControllerHost } from 'lit';
|
||||||
|
import { throttle } from 'lodash-es';
|
||||||
|
import { CameraDimensionsConfig } from '../config/schema/cameras';
|
||||||
|
import { MediaLoadedInfo } from '../types';
|
||||||
|
import { aspectRatioToString, setOrRemoveAttribute } from '../utils/basic';
|
||||||
|
import { AdvancedCameraCardMediaLoadedEventTarget } from '../utils/media-info';
|
||||||
|
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout';
|
||||||
|
|
||||||
|
const SIZE_ATTRIBUTE = 'size';
|
||||||
|
type SizeMode = 'sized' | 'unsized' | 'unsized-portrait' | 'unsized-landscape';
|
||||||
|
|
||||||
|
export class MediaProviderDimensionsController implements ReactiveController {
|
||||||
|
public resize = throttle(this._resizeHandler.bind(this), 100, {
|
||||||
|
trailing: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
private _host: HTMLElement &
|
||||||
|
ReactiveControllerHost &
|
||||||
|
AdvancedCameraCardMediaLoadedEventTarget;
|
||||||
|
private _container: HTMLElement | null = null;
|
||||||
|
private _cameraConfig: CameraDimensionsConfig | null = null;
|
||||||
|
private _resizeObserver = new ResizeObserver(this.resize);
|
||||||
|
private _intendedHostSize: DOMRect | null = null;
|
||||||
|
|
||||||
|
constructor(host: HTMLElement & ReactiveControllerHost) {
|
||||||
|
this._host = host;
|
||||||
|
this._host.addController(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public hostConnected(): void {
|
||||||
|
this._host.addEventListener(
|
||||||
|
'advanced-camera-card:media:loaded',
|
||||||
|
this._mediaLoadHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
this._resizeObserver.observe(this._host);
|
||||||
|
}
|
||||||
|
|
||||||
|
public hostDisconnected(): void {
|
||||||
|
this._host.removeEventListener(
|
||||||
|
'advanced-camera-card:media:loaded',
|
||||||
|
this._mediaLoadHandler,
|
||||||
|
);
|
||||||
|
this._resizeObserver.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setContainer(container?: HTMLElement): void {
|
||||||
|
if (container === this._container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._container = container ?? null;
|
||||||
|
this._setAttributesFromConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setAttributesFromConfig(): void {
|
||||||
|
if (this._container) {
|
||||||
|
this._container.style.aspectRatio = aspectRatioToString({
|
||||||
|
ratio: this._cameraConfig?.aspect_ratio,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateElementStyleFromMediaLayoutConfig(this._host, this._cameraConfig?.layout);
|
||||||
|
|
||||||
|
// When the provider is not precisely sized, we guess the best aspect
|
||||||
|
// ratio to "maximize" if known. This prevents media "hopping" from no
|
||||||
|
// forced aspect ratio to a forced one, once its true size is known.
|
||||||
|
setOrRemoveAttribute<SizeMode>(
|
||||||
|
this._host,
|
||||||
|
true,
|
||||||
|
SIZE_ATTRIBUTE,
|
||||||
|
this._cameraConfig?.aspect_ratio
|
||||||
|
? this._cameraConfig?.aspect_ratio[0] >= this._cameraConfig?.aspect_ratio[1]
|
||||||
|
? 'unsized-landscape'
|
||||||
|
: 'unsized-portrait'
|
||||||
|
: 'unsized',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCameraConfig(config?: CameraDimensionsConfig): void {
|
||||||
|
this._cameraConfig = config ?? null;
|
||||||
|
this._setAttributesFromConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
private _mediaLoadHandler = (_ev: CustomEvent<MediaLoadedInfo>): void => {
|
||||||
|
// Allow the browser to render the media fully before attempting to resize.
|
||||||
|
// Without this, viewer provider will not be sized correctly.
|
||||||
|
window.requestAnimationFrame(() => this.resize());
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
private _resizeHandler(_entries?: ResizeObserverEntry[]): void {
|
||||||
|
const rememberHostSize = (): void => {
|
||||||
|
this._intendedHostSize = this._host.getBoundingClientRect();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setUnsizedAttribute = (): void => {
|
||||||
|
setOrRemoveAttribute<SizeMode>(this._host, true, SIZE_ATTRIBUTE, 'unsized');
|
||||||
|
};
|
||||||
|
|
||||||
|
const setContainerIntrinsicSize = (container: HTMLElement): void => {
|
||||||
|
container.style.width = '100%';
|
||||||
|
container.style.height = 'auto';
|
||||||
|
rememberHostSize();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setContainerSize = (
|
||||||
|
container: HTMLElement,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
): void => {
|
||||||
|
container.style.width = `${width}px`;
|
||||||
|
container.style.height = `${height}px`;
|
||||||
|
rememberHostSize();
|
||||||
|
};
|
||||||
|
|
||||||
|
const hostSize = this._host.getBoundingClientRect();
|
||||||
|
if (
|
||||||
|
hostSize.width === this._intendedHostSize?.width &&
|
||||||
|
hostSize.height === this._intendedHostSize?.height
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._container) {
|
||||||
|
setUnsizedAttribute();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the ideal case, the width can be maximum and the height can be
|
||||||
|
// whatever is necessary to support the aspect ratio.
|
||||||
|
setContainerIntrinsicSize(this._container);
|
||||||
|
|
||||||
|
const containerSize = this._container.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (!containerSize.width || !containerSize.height) {
|
||||||
|
setUnsizedAttribute();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaAspectRatio = containerSize.width / containerSize.height;
|
||||||
|
const newHostSize = this._host.getBoundingClientRect();
|
||||||
|
|
||||||
|
// If the container is larger than the host, the host was not able to expand
|
||||||
|
// enough to cover the size (e.g. fullscreen, panel or height constrained in
|
||||||
|
// configuration). In this case, just limit the container to the host height
|
||||||
|
// at the same aspect ratio.
|
||||||
|
if (containerSize.height > newHostSize.height) {
|
||||||
|
setContainerSize(
|
||||||
|
this._container,
|
||||||
|
newHostSize.height * mediaAspectRatio,
|
||||||
|
newHostSize.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrRemoveAttribute<SizeMode>(this._host, true, SIZE_ATTRIBUTE, 'sized');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,7 +64,7 @@ export const resolveImageMode = (options?: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A media player to wrap a image that updates continuously.
|
* A media player to wrap an image that updates continuously.
|
||||||
*/
|
*/
|
||||||
@customElement('advanced-camera-card-image-updating-player')
|
@customElement('advanced-camera-card-image-updating-player')
|
||||||
export class AdvancedCameraCardImageUpdatingPlayer
|
export class AdvancedCameraCardImageUpdatingPlayer
|
||||||
|
|||||||
+21
-21
@@ -11,6 +11,7 @@ import { guard } from 'lit/directives/guard.js';
|
|||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import { CameraManager } from '../camera-manager/manager';
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
import { ViewManagerEpoch } from '../card-controller/view/types';
|
import { ViewManagerEpoch } from '../card-controller/view/types';
|
||||||
|
import { MediaProviderDimensionsController } from '../components-lib/media-provider-dimensions-controller';
|
||||||
import { ZoomSettingsObserved } from '../components-lib/zoom/types';
|
import { ZoomSettingsObserved } from '../components-lib/zoom/types';
|
||||||
import { handleZoomSettingsObservedEvent } from '../components-lib/zoom/zoom-view-context';
|
import { handleZoomSettingsObservedEvent } from '../components-lib/zoom/zoom-view-context';
|
||||||
import { CameraConfig } from '../config/schema/cameras';
|
import { CameraConfig } from '../config/schema/cameras';
|
||||||
@@ -19,8 +20,6 @@ import { IMAGE_VIEW_ZOOM_TARGET_SENTINEL } from '../const';
|
|||||||
import { HomeAssistant } from '../ha/types';
|
import { HomeAssistant } from '../ha/types';
|
||||||
import imageStyle from '../scss/image.scss';
|
import imageStyle from '../scss/image.scss';
|
||||||
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../types.js';
|
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../types.js';
|
||||||
import { aspectRatioToString } from '../utils/basic';
|
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
|
||||||
import './image-updating-player';
|
import './image-updating-player';
|
||||||
import { resolveImageMode } from './image-updating-player';
|
import { resolveImageMode } from './image-updating-player';
|
||||||
import './zoomer.js';
|
import './zoomer.js';
|
||||||
@@ -42,36 +41,29 @@ export class AdvancedCameraCardImage extends LitElement implements MediaPlayer {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public imageConfig?: ImageViewConfig;
|
public imageConfig?: ImageViewConfig;
|
||||||
|
|
||||||
|
protected _dimensionsController = new MediaProviderDimensionsController(this);
|
||||||
|
protected _refImage: Ref<MediaPlayerElement> = createRef();
|
||||||
|
protected _refContainer: Ref<HTMLElement> = createRef();
|
||||||
|
|
||||||
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
return (await this._refImage.value?.getMediaPlayerController()) ?? null;
|
return (await this._refImage.value?.getMediaPlayerController()) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _refImage: Ref<MediaPlayerElement> = createRef();
|
|
||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
if (changedProps.has('cameraConfig') || changedProps.has('imageConfig')) {
|
if (changedProps.has('cameraConfig') || changedProps.has('imageConfig')) {
|
||||||
if (
|
this._dimensionsController.setCameraConfig(
|
||||||
resolveImageMode({
|
resolveImageMode({
|
||||||
imageConfig: this.imageConfig,
|
imageConfig: this.imageConfig,
|
||||||
cameraConfig: this.cameraConfig,
|
cameraConfig: this.cameraConfig,
|
||||||
}) === 'camera'
|
}) === 'camera'
|
||||||
) {
|
? this.cameraConfig?.dimensions
|
||||||
updateElementStyleFromMediaLayoutConfig(
|
: undefined,
|
||||||
this,
|
|
||||||
this.cameraConfig?.dimensions?.layout,
|
|
||||||
);
|
);
|
||||||
this.style.aspectRatio = aspectRatioToString({
|
|
||||||
ratio: this.cameraConfig?.dimensions?.aspect_ratio,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
updateElementStyleFromMediaLayoutConfig(this);
|
|
||||||
this.style.removeProperty('aspect-ratio');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _useZoomIfRequired(template: TemplateResult): TemplateResult {
|
protected _renderContainer(template: TemplateResult): TemplateResult {
|
||||||
const zoomTarget = IMAGE_VIEW_ZOOM_TARGET_SENTINEL;
|
const zoomTarget = IMAGE_VIEW_ZOOM_TARGET_SENTINEL;
|
||||||
const view = this.viewManagerEpoch?.manager.getView();
|
const view = this.viewManagerEpoch?.manager.getView();
|
||||||
const mode = resolveImageMode({
|
const mode = resolveImageMode({
|
||||||
@@ -79,7 +71,8 @@ export class AdvancedCameraCardImage extends LitElement implements MediaPlayer {
|
|||||||
cameraConfig: this.cameraConfig,
|
cameraConfig: this.cameraConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.imageConfig?.zoomable
|
return html`<div class="container" ${ref(this._refContainer)}>
|
||||||
|
${this.imageConfig?.zoomable
|
||||||
? html`<advanced-camera-card-zoomer
|
? html`<advanced-camera-card-zoomer
|
||||||
.defaultSettings=${guard(
|
.defaultSettings=${guard(
|
||||||
[this.imageConfig, this.cameraConfig?.dimensions?.layout],
|
[this.imageConfig, this.cameraConfig?.dimensions?.layout],
|
||||||
@@ -92,7 +85,9 @@ export class AdvancedCameraCardImage extends LitElement implements MediaPlayer {
|
|||||||
: undefined,
|
: undefined,
|
||||||
)}
|
)}
|
||||||
.settings=${view?.context?.zoom?.[zoomTarget]?.requested}
|
.settings=${view?.context?.zoom?.[zoomTarget]?.requested}
|
||||||
@advanced-camera-card:zoom:change=${(ev: CustomEvent<ZoomSettingsObserved>) =>
|
@advanced-camera-card:zoom:change=${(
|
||||||
|
ev: CustomEvent<ZoomSettingsObserved>,
|
||||||
|
) =>
|
||||||
handleZoomSettingsObservedEvent(
|
handleZoomSettingsObservedEvent(
|
||||||
ev,
|
ev,
|
||||||
this.viewManagerEpoch?.manager,
|
this.viewManagerEpoch?.manager,
|
||||||
@@ -101,7 +96,8 @@ export class AdvancedCameraCardImage extends LitElement implements MediaPlayer {
|
|||||||
>
|
>
|
||||||
${template}
|
${template}
|
||||||
</advanced-camera-card-zoomer>`
|
</advanced-camera-card-zoomer>`
|
||||||
: template;
|
: template}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
@@ -109,7 +105,7 @@ export class AdvancedCameraCardImage extends LitElement implements MediaPlayer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._useZoomIfRequired(html`
|
return this._renderContainer(html`
|
||||||
<advanced-camera-card-image-updating-player
|
<advanced-camera-card-image-updating-player
|
||||||
${ref(this._refImage)}
|
${ref(this._refImage)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -121,6 +117,10 @@ export class AdvancedCameraCardImage extends LitElement implements MediaPlayer {
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updated(): void {
|
||||||
|
this._dimensionsController.setContainer(this._refContainer.value);
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(imageStyle);
|
return unsafeCSS(imageStyle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,12 +40,6 @@ export class AdvancedCameraCardLive extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementation notes:
|
|
||||||
// - See use of liveConfig and not config below -- the underlying carousel
|
|
||||||
// will independently override the liveConfig to reflect the camera in the
|
|
||||||
// carousel (not necessarily the selected camera).
|
|
||||||
// - Various events are captured to prevent them propagating upwards if the
|
|
||||||
// card is in the background.
|
|
||||||
return html`
|
return html`
|
||||||
<advanced-camera-card-live-grid
|
<advanced-camera-card-live-grid
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { CameraEndpoints } from '../../camera-manager/types.js';
|
|||||||
import { MicrophoneState } from '../../card-controller/types.js';
|
import { MicrophoneState } from '../../card-controller/types.js';
|
||||||
import { LazyLoadController } from '../../components-lib/lazy-load-controller.js';
|
import { LazyLoadController } from '../../components-lib/lazy-load-controller.js';
|
||||||
import { dispatchLiveErrorEvent } from '../../components-lib/live/utils/dispatch-live-error.js';
|
import { dispatchLiveErrorEvent } from '../../components-lib/live/utils/dispatch-live-error.js';
|
||||||
|
import { MediaProviderDimensionsController } from '../../components-lib/media-provider-dimensions-controller.js';
|
||||||
import { PartialZoomSettings } from '../../components-lib/zoom/types.js';
|
import { PartialZoomSettings } from '../../components-lib/zoom/types.js';
|
||||||
import { CameraConfig, LiveProvider } from '../../config/schema/cameras.js';
|
import { CameraConfig, LiveProvider } from '../../config/schema/cameras.js';
|
||||||
import { LiveConfig } from '../../config/schema/live.js';
|
import { LiveConfig } from '../../config/schema/live.js';
|
||||||
@@ -22,15 +23,15 @@ import { STREAM_TROUBLESHOOTING_URL } from '../../const.js';
|
|||||||
import { HomeAssistant } from '../../ha/types.js';
|
import { HomeAssistant } from '../../ha/types.js';
|
||||||
import { localize } from '../../localize/localize.js';
|
import { localize } from '../../localize/localize.js';
|
||||||
import liveProviderStyle from '../../scss/live-provider.scss';
|
import liveProviderStyle from '../../scss/live-provider.scss';
|
||||||
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../../types.js';
|
import {
|
||||||
import { aspectRatioToString } from '../../utils/basic.js';
|
MediaLoadedInfo,
|
||||||
|
MediaPlayer,
|
||||||
|
MediaPlayerController,
|
||||||
|
MediaPlayerElement,
|
||||||
|
} from '../../types.js';
|
||||||
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
|
||||||
import '../icon.js';
|
import '../icon.js';
|
||||||
import { renderMessage } from '../message.js';
|
import { renderMessage } from '../message.js';
|
||||||
import '../next-prev-control.js';
|
|
||||||
import '../ptz.js';
|
|
||||||
import '../surround.js';
|
|
||||||
|
|
||||||
@customElement('advanced-camera-card-live-provider')
|
@customElement('advanced-camera-card-live-provider')
|
||||||
export class AdvancedCameraCardLiveProvider extends LitElement implements MediaPlayer {
|
export class AdvancedCameraCardLiveProvider extends LitElement implements MediaPlayer {
|
||||||
@@ -69,7 +70,9 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
protected _showStreamTroubleshooting = false;
|
protected _showStreamTroubleshooting = false;
|
||||||
|
|
||||||
protected _refProvider: Ref<MediaPlayerElement> = createRef();
|
protected _refProvider: Ref<MediaPlayerElement> = createRef();
|
||||||
|
protected _refContainer: Ref<HTMLElement> = createRef();
|
||||||
protected _lazyLoadController: LazyLoadController = new LazyLoadController(this);
|
protected _lazyLoadController: LazyLoadController = new LazyLoadController(this);
|
||||||
|
protected _dimensionsController = new MediaProviderDimensionsController(this);
|
||||||
|
|
||||||
// A note on dynamic imports:
|
// A note on dynamic imports:
|
||||||
//
|
//
|
||||||
@@ -184,13 +187,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
this._importPromises.push(import('./providers/go2rtc/index.js'));
|
this._importPromises.push(import('./providers/go2rtc/index.js'));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateElementStyleFromMediaLayoutConfig(
|
this._dimensionsController.setCameraConfig(this.cameraConfig?.dimensions);
|
||||||
this,
|
|
||||||
this.cameraConfig?.dimensions?.layout,
|
|
||||||
);
|
|
||||||
this.style.aspectRatio = aspectRatioToString({
|
|
||||||
ratio: this.cameraConfig?.dimensions?.aspect_ratio,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,8 +200,11 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _useZoomIfRequired(template: TemplateResult): TemplateResult {
|
protected _renderContainer(template: TemplateResult): TemplateResult {
|
||||||
return this.liveConfig?.zoomable
|
// Place the zoomer in a separate div, as the zoom library misinterprets the
|
||||||
|
// explicit width/height setting from the provider resizer as zooming.
|
||||||
|
return html`<div class="container" ${ref(this._refContainer)}>
|
||||||
|
${this.liveConfig?.zoomable
|
||||||
? html` <advanced-camera-card-zoomer
|
? html` <advanced-camera-card-zoomer
|
||||||
.defaultSettings=${guard([this.cameraConfig?.dimensions?.layout], () =>
|
.defaultSettings=${guard([this.cameraConfig?.dimensions?.layout], () =>
|
||||||
this.cameraConfig?.dimensions?.layout
|
this.cameraConfig?.dimensions?.layout
|
||||||
@@ -222,7 +222,8 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
>
|
>
|
||||||
${template}
|
${template}
|
||||||
</advanced-camera-card-zoomer>`
|
</advanced-camera-card-zoomer>`
|
||||||
: template;
|
: template}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
@@ -240,11 +241,6 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
this.ariaLabel = this.label;
|
this.ariaLabel = this.label;
|
||||||
|
|
||||||
const provider = this._getResolvedProvider();
|
const provider = this._getResolvedProvider();
|
||||||
const showImageDuringLoading = this._shouldShowImageDuringLoading();
|
|
||||||
const showLoadingIcon = !this._isVideoMediaLoaded;
|
|
||||||
const providerClasses = {
|
|
||||||
hidden: showImageDuringLoading,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
provider === 'ha' ||
|
provider === 'ha' ||
|
||||||
@@ -287,20 +283,36 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`${this._useZoomIfRequired(html`
|
const showImageDuringLoading = this._shouldShowImageDuringLoading();
|
||||||
|
const showLoadingIcon = !this._isVideoMediaLoaded;
|
||||||
|
|
||||||
|
const classes = {
|
||||||
|
hidden: showImageDuringLoading,
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`${this._renderContainer(html`
|
||||||
${showImageDuringLoading || provider === 'image'
|
${showImageDuringLoading || provider === 'image'
|
||||||
? html` <advanced-camera-card-live-image
|
? html` <advanced-camera-card-live-image
|
||||||
${ref(this._refProvider)}
|
${ref(this._refProvider)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
|
class=${classMap({
|
||||||
|
...classes,
|
||||||
|
// The image provider is providing the temporary loading image,
|
||||||
|
// so it should not be hidden.
|
||||||
|
hidden: false,
|
||||||
|
})}
|
||||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||||
@advanced-camera-card:media:loaded=${(ev: Event) => {
|
@advanced-camera-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||||
if (provider === 'image') {
|
if (provider === 'image') {
|
||||||
// Only count the media has loaded if the required provider is
|
// Only count the media has loaded if the required provider is
|
||||||
// the image (not just the temporary image shown during
|
// the image (not just the temporary image shown during
|
||||||
// loading).
|
// loading).
|
||||||
this._videoMediaShowHandler();
|
this._videoMediaShowHandler();
|
||||||
} else {
|
} else {
|
||||||
|
// Manually call resize(), since the dimensions controller won't
|
||||||
|
// receive the after that stopPropagation().
|
||||||
|
this._dimensionsController.resize();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -310,7 +322,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
${provider === 'ha'
|
${provider === 'ha'
|
||||||
? html` <advanced-camera-card-live-ha
|
? html` <advanced-camera-card-live-ha
|
||||||
${ref(this._refProvider)}
|
${ref(this._refProvider)}
|
||||||
class=${classMap(providerClasses)}
|
class=${classMap(classes)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
?controls=${this.liveConfig.controls.builtin}
|
?controls=${this.liveConfig.controls.builtin}
|
||||||
@@ -321,7 +333,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
: provider === 'go2rtc'
|
: provider === 'go2rtc'
|
||||||
? html`<advanced-camera-card-live-go2rtc
|
? html`<advanced-camera-card-live-go2rtc
|
||||||
${ref(this._refProvider)}
|
${ref(this._refProvider)}
|
||||||
class=${classMap(providerClasses)}
|
class=${classMap(classes)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
.cameraEndpoints=${this.cameraEndpoints}
|
.cameraEndpoints=${this.cameraEndpoints}
|
||||||
@@ -337,7 +349,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
: provider === 'webrtc-card'
|
: provider === 'webrtc-card'
|
||||||
? html`<advanced-camera-card-live-webrtc-card
|
? html`<advanced-camera-card-live-webrtc-card
|
||||||
${ref(this._refProvider)}
|
${ref(this._refProvider)}
|
||||||
class=${classMap(providerClasses)}
|
class=${classMap(classes)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
.cameraEndpoints=${this.cameraEndpoints}
|
.cameraEndpoints=${this.cameraEndpoints}
|
||||||
@@ -352,7 +364,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
: provider === 'jsmpeg'
|
: provider === 'jsmpeg'
|
||||||
? html` <advanced-camera-card-live-jsmpeg
|
? html` <advanced-camera-card-live-jsmpeg
|
||||||
${ref(this._refProvider)}
|
${ref(this._refProvider)}
|
||||||
class=${classMap(providerClasses)}
|
class=${classMap(classes)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
.cameraEndpoints=${this.cameraEndpoints}
|
.cameraEndpoints=${this.cameraEndpoints}
|
||||||
@@ -390,6 +402,10 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
: ''}`;
|
: ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updated(): void {
|
||||||
|
this._dimensionsController.setContainer(this._refContainer.value);
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(liveProviderStyle);
|
return unsafeCSS(liveProviderStyle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
|||||||
import { MediaGridSelected } from '../../components-lib/media-grid-controller.js';
|
import { MediaGridSelected } from '../../components-lib/media-grid-controller.js';
|
||||||
import { CardWideConfig } from '../../config/schema/types.js';
|
import { CardWideConfig } from '../../config/schema/types.js';
|
||||||
import { ViewerConfig } from '../../config/schema/viewer.js';
|
import { ViewerConfig } from '../../config/schema/viewer.js';
|
||||||
import { HomeAssistant } from '../../ha/types.js';
|
|
||||||
import { ResolvedMediaCache } from '../../ha/resolved-media.js';
|
import { ResolvedMediaCache } from '../../ha/resolved-media.js';
|
||||||
|
import { HomeAssistant } from '../../ha/types.js';
|
||||||
import '../../patches/ha-hls-player.js';
|
import '../../patches/ha-hls-player.js';
|
||||||
import basicBlockStyle from '../../scss/basic-block.scss';
|
import basicBlockStyle from '../../scss/basic-block.scss';
|
||||||
import './carousel';
|
import './carousel';
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
|||||||
import { CameraManager } from '../../camera-manager/manager.js';
|
import { CameraManager } from '../../camera-manager/manager.js';
|
||||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||||
import { LazyLoadController } from '../../components-lib/lazy-load-controller.js';
|
import { LazyLoadController } from '../../components-lib/lazy-load-controller.js';
|
||||||
|
import { MediaProviderDimensionsController } from '../../components-lib/media-provider-dimensions-controller.js';
|
||||||
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
||||||
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
||||||
|
import { CameraConfig } from '../../config/schema/cameras.js';
|
||||||
import { CardWideConfig } from '../../config/schema/types.js';
|
import { CardWideConfig } from '../../config/schema/types.js';
|
||||||
import { ViewerConfig } from '../../config/schema/viewer.js';
|
import { ViewerConfig } from '../../config/schema/viewer.js';
|
||||||
import { canonicalizeHAURL } from '../../ha/canonical-url.js';
|
import { canonicalizeHAURL } from '../../ha/canonical-url.js';
|
||||||
@@ -29,8 +31,7 @@ import {
|
|||||||
import '../../patches/ha-hls-player.js';
|
import '../../patches/ha-hls-player.js';
|
||||||
import viewerProviderStyle from '../../scss/viewer-provider.scss';
|
import viewerProviderStyle from '../../scss/viewer-provider.scss';
|
||||||
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../../types.js';
|
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../../types.js';
|
||||||
import { aspectRatioToString, errorToConsole } from '../../utils/basic.js';
|
import { errorToConsole } from '../../utils/basic.js';
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
|
||||||
import { ViewItemClassifier } from '../../view/item-classifier.js';
|
import { ViewItemClassifier } from '../../view/item-classifier.js';
|
||||||
import { VideoContentType, ViewMedia } from '../../view/item.js';
|
import { VideoContentType, ViewMedia } from '../../view/item.js';
|
||||||
import { QueryClassifier } from '../../view/query-classifier.js';
|
import { QueryClassifier } from '../../view/query-classifier.js';
|
||||||
@@ -62,7 +63,9 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
|||||||
public cardWideConfig?: CardWideConfig;
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
protected _refProvider: Ref<MediaPlayerElement> = createRef();
|
protected _refProvider: Ref<MediaPlayerElement> = createRef();
|
||||||
|
protected _refContainer: Ref<HTMLElement> = createRef();
|
||||||
protected _lazyLoadController: LazyLoadController = new LazyLoadController(this);
|
protected _lazyLoadController: LazyLoadController = new LazyLoadController(this);
|
||||||
|
protected _dimensionsController = new MediaProviderDimensionsController(this);
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
protected _url: string | null = null;
|
protected _url: string | null = null;
|
||||||
@@ -200,19 +203,20 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (changedProps.has('media') || changedProps.has('cameraManager')) {
|
if (changedProps.has('media') || changedProps.has('cameraManager')) {
|
||||||
|
this._dimensionsController.setCameraConfig(
|
||||||
|
this._getRelevantCameraConfig()?.dimensions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getRelevantCameraConfig(): CameraConfig | null {
|
||||||
const cameraID = this.media?.getCameraID();
|
const cameraID = this.media?.getCameraID();
|
||||||
const cameraConfig = cameraID
|
return cameraID
|
||||||
? this.cameraManager?.getStore().getCameraConfig(cameraID)
|
? this.cameraManager?.getStore().getCameraConfig(cameraID) ?? null
|
||||||
: null;
|
: null;
|
||||||
updateElementStyleFromMediaLayoutConfig(this, cameraConfig?.dimensions?.layout);
|
|
||||||
|
|
||||||
this.style.aspectRatio = aspectRatioToString({
|
|
||||||
ratio: cameraConfig?.dimensions?.aspect_ratio,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _useZoomIfRequired(template: TemplateResult): TemplateResult {
|
protected _renderContainer(template: TemplateResult): TemplateResult {
|
||||||
if (!this.media) {
|
if (!this.media) {
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
@@ -223,7 +227,10 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
|||||||
: null;
|
: null;
|
||||||
const view = this.viewManagerEpoch?.manager.getView();
|
const view = this.viewManagerEpoch?.manager.getView();
|
||||||
|
|
||||||
return this.viewerConfig?.zoomable
|
// Place the zoomer in a separate div, as the zoom library misinterprets the
|
||||||
|
// explicit width/height setting from the provider resizer as zooming.
|
||||||
|
return html`<div class="container" ${ref(this._refContainer)}>
|
||||||
|
${this.viewerConfig?.zoomable
|
||||||
? html`<advanced-camera-card-zoomer
|
? html`<advanced-camera-card-zoomer
|
||||||
.defaultSettings=${guard([cameraConfig?.dimensions?.layout], () =>
|
.defaultSettings=${guard([cameraConfig?.dimensions?.layout], () =>
|
||||||
cameraConfig?.dimensions?.layout
|
cameraConfig?.dimensions?.layout
|
||||||
@@ -238,12 +245,19 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
|||||||
(await this.getMediaPlayerController())?.setControls(false)}
|
(await this.getMediaPlayerController())?.setControls(false)}
|
||||||
@advanced-camera-card:zoom:unzoomed=${async () =>
|
@advanced-camera-card:zoom:unzoomed=${async () =>
|
||||||
(await this.getMediaPlayerController())?.setControls()}
|
(await this.getMediaPlayerController())?.setControls()}
|
||||||
@advanced-camera-card:zoom:change=${(ev: CustomEvent<ZoomSettingsObserved>) =>
|
@advanced-camera-card:zoom:change=${(
|
||||||
handleZoomSettingsObservedEvent(ev, this.viewManagerEpoch?.manager, mediaID)}
|
ev: CustomEvent<ZoomSettingsObserved>,
|
||||||
|
) =>
|
||||||
|
handleZoomSettingsObservedEvent(
|
||||||
|
ev,
|
||||||
|
this.viewManagerEpoch?.manager,
|
||||||
|
mediaID,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
${template}
|
${template}
|
||||||
</advanced-camera-card-zoomer>`
|
</advanced-camera-card-zoomer>`
|
||||||
: template;
|
: template}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
@@ -264,7 +278,7 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
|||||||
|
|
||||||
// Note: crossorigin="anonymous" is required on <video> below in order to
|
// Note: crossorigin="anonymous" is required on <video> below in order to
|
||||||
// allow screenshot of motionEye videos which currently go cross-origin.
|
// allow screenshot of motionEye videos which currently go cross-origin.
|
||||||
return this._useZoomIfRequired(html`
|
return this._renderContainer(html`
|
||||||
${ViewItemClassifier.isVideo(this.media)
|
${ViewItemClassifier.isVideo(this.media)
|
||||||
? this.media.getVideoContentType() === VideoContentType.HLS
|
? this.media.getVideoContentType() === VideoContentType.HLS
|
||||||
? html`<advanced-camera-card-ha-hls-player
|
? html`<advanced-camera-card-ha-hls-player
|
||||||
@@ -305,6 +319,10 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updated(): void {
|
||||||
|
this._dimensionsController.setContainer(this._refContainer.value);
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(viewerProviderStyle);
|
return unsafeCSS(viewerProviderStyle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { CameraManager } from '../camera-manager/manager.js';
|
|||||||
import { MicrophoneState } from '../card-controller/types.js';
|
import { MicrophoneState } from '../card-controller/types.js';
|
||||||
import { ViewItemManager } from '../card-controller/view/item-manager.js';
|
import { ViewItemManager } from '../card-controller/view/item-manager.js';
|
||||||
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
||||||
|
import { ConditionStateManagerReadonlyInterface } from '../conditions/types.js';
|
||||||
import { AdvancedCameraCardConfig, CardWideConfig } from '../config/schema/types.js';
|
import { AdvancedCameraCardConfig, CardWideConfig } from '../config/schema/types.js';
|
||||||
import { RawAdvancedCameraCardConfig } from '../config/types.js';
|
import { RawAdvancedCameraCardConfig } from '../config/types.js';
|
||||||
import { DeviceRegistryManager } from '../ha/registry/device/index.js';
|
import { DeviceRegistryManager } from '../ha/registry/device/index.js';
|
||||||
@@ -62,6 +63,9 @@ export class AdvancedCameraCardViews extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public deviceRegistryManager?: DeviceRegistryManager;
|
public deviceRegistryManager?: DeviceRegistryManager;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public conditionStateManager?: ConditionStateManagerReadonlyInterface;
|
||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
if (changedProps.has('viewManagerEpoch') || changedProps.has('config')) {
|
if (changedProps.has('viewManagerEpoch') || changedProps.has('config')) {
|
||||||
const view = this.viewManagerEpoch?.manager.getView();
|
const view = this.viewManagerEpoch?.manager.getView();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface ConditionState {
|
|||||||
keys?: KeysState;
|
keys?: KeysState;
|
||||||
mediaLoadedInfo?: MediaLoadedInfo | null;
|
mediaLoadedInfo?: MediaLoadedInfo | null;
|
||||||
microphone?: MicrophoneState;
|
microphone?: MicrophoneState;
|
||||||
|
panel?: boolean;
|
||||||
hass?: HomeAssistant;
|
hass?: HomeAssistant;
|
||||||
triggered?: Set<string>;
|
triggered?: Set<string>;
|
||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
|
|||||||
@@ -146,6 +146,12 @@ const proxyConfigSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type ProxyConfig = z.infer<typeof proxyConfigSchema>;
|
export type ProxyConfig = z.infer<typeof proxyConfigSchema>;
|
||||||
|
|
||||||
|
const cameraDimensionsSchema = z.object({
|
||||||
|
aspect_ratio: aspectRatioSchema.optional(),
|
||||||
|
layout: mediaLayoutConfigSchema.optional(),
|
||||||
|
});
|
||||||
|
export type CameraDimensionsConfig = z.infer<typeof cameraDimensionsSchema>;
|
||||||
|
|
||||||
export const cameraConfigSchema = z
|
export const cameraConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
camera_entity: z.string().optional(),
|
camera_entity: z.string().optional(),
|
||||||
@@ -246,12 +252,7 @@ export const cameraConfigSchema = z
|
|||||||
|
|
||||||
ptz: ptzCameraConfigSchema.default(cameraConfigDefault.ptz),
|
ptz: ptzCameraConfigSchema.default(cameraConfigDefault.ptz),
|
||||||
|
|
||||||
dimensions: z
|
dimensions: cameraDimensionsSchema.optional(),
|
||||||
.object({
|
|
||||||
aspect_ratio: aspectRatioSchema.optional(),
|
|
||||||
layout: mediaLayoutConfigSchema.optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
proxy: proxyConfigSchema.default(cameraConfigDefault.proxy),
|
proxy: proxyConfigSchema.default(cameraConfigDefault.proxy),
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
// Different browsers use different colors as their fullscreen background,
|
// Different browsers use different colors as their fullscreen background,
|
||||||
// this ensures the same experience across all browsers.
|
// this ensures the same experience across all browsers.
|
||||||
background-color: var(--card-background-color);
|
background-color: var(--advanced-camera-card-background);
|
||||||
|
|
||||||
border-radius: var(--ha-card-border-radius, 4px);
|
border-radius: var(--ha-card-border-radius, 4px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -99,6 +99,9 @@ ha-card {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
position: static;
|
position: static;
|
||||||
color: var(--secondary-text-color, white);
|
color: var(--secondary-text-color, white);
|
||||||
|
|
||||||
|
// The background color at the outer level has priority.
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/************
|
/************
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
@use 'basic-block.scss';
|
@use 'basic-block.scss';
|
||||||
@use 'media-background.scss';
|
|
||||||
@use 'media-layout.scss';
|
@use 'media-layout.scss';
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
|||||||
+1
-11
@@ -1,11 +1 @@
|
|||||||
@use 'basic-block.scss';
|
@use 'provider.scss';
|
||||||
@use 'media-layout.scss';
|
|
||||||
@use 'media-background.scss';
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
@include media-layout.media-layout();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,10 +24,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.embla__slide {
|
.embla__slide {
|
||||||
// Center the content horizontally (for cases where the configured aspect
|
|
||||||
// ratio is a mismatch with the card).
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
@use 'basic-block.scss';
|
@use 'provider.scss';
|
||||||
@use 'media-background.scss';
|
|
||||||
|
|
||||||
:host {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
$bg-img: url('../images/iris-background.svg');
|
$bg-img: url('../images/iris-background.svg');
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
background-color: var(--primary-background-color);
|
background-color: var(--advanced-camera-card-background);
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-image: $bg-img;
|
background-image: $bg-img;
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
@use 'media-background.scss';
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
// The container will be sized by the provider resize controller.
|
||||||
|
width: fit-content;
|
||||||
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([size='unsized']) > .container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
:host([size='unsized-portrait']) > .container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
:host([size='unsized-landscape']) > .container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
*********/
|
*********/
|
||||||
|
|
||||||
--advanced-camera-card-exterior: var(--secondary-background-color, black);
|
--advanced-camera-card-exterior: var(--secondary-background-color, black);
|
||||||
--advanced-camera-card-background: var(--primary-background-color, white);
|
--advanced-camera-card-background: var(--card-background-color, white);
|
||||||
|
|
||||||
--advanced-camera-card-foreground-primary: var(--primary-color, black);
|
--advanced-camera-card-foreground-primary: var(--primary-color, black);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// This file should include only, and exactly match, values from
|
// This file should include only, and exactly match, values from
|
||||||
// https://github.com/home-assistant/frontend/blob/dev/src/resources/styles-data.ts .
|
// https://github.com/home-assistant/frontend/blob/dev/src/resources/styles-data.ts .
|
||||||
|
|
||||||
|
--card-background-color: #1c1c1c;
|
||||||
--primary-background-color: #111111;
|
--primary-background-color: #111111;
|
||||||
--secondary-background-color: #282828;
|
--secondary-background-color: #282828;
|
||||||
--primary-text-color: #e1e1e1;
|
--primary-text-color: #e1e1e1;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// This file should include only, and exactly match, values from
|
// This file should include only, and exactly match, values from
|
||||||
// https://github.com/home-assistant/frontend/blob/dev/src/resources/ha-style.ts
|
// https://github.com/home-assistant/frontend/blob/dev/src/resources/ha-style.ts
|
||||||
|
|
||||||
|
--card-background-color: #ffffff;
|
||||||
--primary-background-color: #fafafa;
|
--primary-background-color: #fafafa;
|
||||||
--secondary-background-color: #e5e5e5;
|
--secondary-background-color: #e5e5e5;
|
||||||
--primary-text-color: #212121;
|
--primary-text-color: #212121;
|
||||||
|
|||||||
@@ -41,6 +41,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.embla__slide {
|
.embla__slide {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,4 @@
|
|||||||
@use 'basic-block.scss';
|
@use 'provider.scss';
|
||||||
@use 'media-background.scss';
|
|
||||||
|
|
||||||
advanced-camera-card-ha-hls-player,
|
|
||||||
advanced-camera-card-image-player,
|
|
||||||
advanced-camera-card-video-player {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
advanced-camera-card-progress-indicator {
|
advanced-camera-card-progress-indicator {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
|
|||||||
+2
-2
@@ -196,11 +196,11 @@ export const isValidDate = (date: Date): boolean => {
|
|||||||
* @param name The attribute name.
|
* @param name The attribute name.
|
||||||
* @param value An optional value to set the attribute to.
|
* @param value An optional value to set the attribute to.
|
||||||
*/
|
*/
|
||||||
export const setOrRemoveAttribute = (
|
export const setOrRemoveAttribute = <T extends string>(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
set: boolean,
|
set: boolean,
|
||||||
name: string,
|
name: string,
|
||||||
value?: string,
|
value?: T,
|
||||||
): void => {
|
): void => {
|
||||||
if (set) {
|
if (set) {
|
||||||
element.setAttribute(name, value ?? '');
|
element.setAttribute(name, value ?? '');
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { scrollIntoView } from '../../../src/utils/scroll';
|
|||||||
import { sleep } from '../../../src/utils/sleep';
|
import { sleep } from '../../../src/utils/sleep';
|
||||||
import {
|
import {
|
||||||
callIntersectionHandler,
|
callIntersectionHandler,
|
||||||
|
callResizeHandler,
|
||||||
createLitElement,
|
createLitElement,
|
||||||
createSlot,
|
createSlot,
|
||||||
createSlotHost,
|
createSlotHost,
|
||||||
@@ -18,7 +19,6 @@ import {
|
|||||||
IntersectionObserverMock,
|
IntersectionObserverMock,
|
||||||
ResizeObserverMock,
|
ResizeObserverMock,
|
||||||
} from '../../test-utils';
|
} from '../../test-utils';
|
||||||
import { callResizeHandler } from '../../utils/embla/test-utils';
|
|
||||||
|
|
||||||
vi.mock('lodash-es', async () => {
|
vi.mock('lodash-es', async () => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vites
|
|||||||
import { MediaHeightController } from '../../src/components-lib/media-height-controller';
|
import { MediaHeightController } from '../../src/components-lib/media-height-controller';
|
||||||
import {
|
import {
|
||||||
callMutationHandler,
|
callMutationHandler,
|
||||||
|
callResizeHandler,
|
||||||
MutationObserverMock,
|
MutationObserverMock,
|
||||||
ResizeObserverMock,
|
ResizeObserverMock,
|
||||||
} from '../test-utils';
|
} from '../test-utils';
|
||||||
import { callResizeHandler } from '../utils/embla/test-utils';
|
|
||||||
|
|
||||||
vi.mock('lodash-es', async () => ({
|
vi.mock('lodash-es', async () => ({
|
||||||
...(await vi.importActual('lodash-es')),
|
...(await vi.importActual('lodash-es')),
|
||||||
|
|||||||
@@ -0,0 +1,391 @@
|
|||||||
|
import {
|
||||||
|
afterAll,
|
||||||
|
afterEach,
|
||||||
|
beforeAll,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
vi,
|
||||||
|
} from 'vitest';
|
||||||
|
import { MediaProviderDimensionsController } from '../../src/components-lib/media-provider-dimensions-controller';
|
||||||
|
import { CameraDimensionsConfig } from '../../src/config/schema/cameras';
|
||||||
|
import { dispatchExistingMediaLoadedInfoAsEvent } from '../../src/utils/media-info';
|
||||||
|
import {
|
||||||
|
callResizeHandler,
|
||||||
|
createLitElement,
|
||||||
|
createMediaLoadedInfo,
|
||||||
|
getResizeObserver,
|
||||||
|
requestAnimationFrameMock,
|
||||||
|
ResizeObserverMock,
|
||||||
|
} from '../test-utils';
|
||||||
|
|
||||||
|
vi.mock('lodash-es', () => ({
|
||||||
|
throttle: vi.fn((fn) => fn),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('MediaProviderDimensionsController', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.stubGlobal('ResizeObserver', ResizeObserverMock);
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should construct', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const eventListener = vi.fn();
|
||||||
|
host.addEventListener = eventListener;
|
||||||
|
|
||||||
|
new MediaProviderDimensionsController(host);
|
||||||
|
|
||||||
|
const observer = getResizeObserver();
|
||||||
|
|
||||||
|
// No resize observer should be created.
|
||||||
|
expect(observer?.observe).not.toBeCalled();
|
||||||
|
expect(eventListener).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should connect and disconnect', () => {
|
||||||
|
it('should connect and disconnect without a container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
|
||||||
|
const observer = getResizeObserver();
|
||||||
|
expect(observer?.observe).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
controller.hostConnected();
|
||||||
|
expect(observer?.observe).toBeCalledWith(host);
|
||||||
|
expect(observer?.observe).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
controller.hostDisconnected();
|
||||||
|
expect(observer?.disconnect).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should connect and disconnect with a container when host is connected', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
Object.defineProperty(host, 'isConnected', {
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
|
||||||
|
const observer = getResizeObserver(0);
|
||||||
|
|
||||||
|
const container = createLitElement();
|
||||||
|
controller.setContainer(container);
|
||||||
|
|
||||||
|
expect(observer?.observe).not.toBeCalled();
|
||||||
|
|
||||||
|
controller.hostDisconnected();
|
||||||
|
expect(observer?.disconnect).toBeCalled();
|
||||||
|
|
||||||
|
controller.hostConnected();
|
||||||
|
expect(observer?.observe).toBeCalledWith(host);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should set container respecting config ', () => {
|
||||||
|
it('should set aspect ratio on container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
|
||||||
|
const config = { aspect_ratio: [16, 9] };
|
||||||
|
controller.setCameraConfig(config);
|
||||||
|
controller.setContainer(container);
|
||||||
|
|
||||||
|
expect(container.style.aspectRatio).toBe('16 / 9');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should set host attribute', () => {
|
||||||
|
it.each([
|
||||||
|
['unsized', {}],
|
||||||
|
['unsized-landscape', { aspect_ratio: [16, 9] }],
|
||||||
|
['unsized-portrait', { aspect_ratio: [9, 16] }],
|
||||||
|
])('%s', async (value: string, config: CameraDimensionsConfig) => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
controller.setCameraConfig(config);
|
||||||
|
|
||||||
|
controller.setContainer(container);
|
||||||
|
|
||||||
|
expect(host.getAttribute('size')).toBe(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set layout attributes', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
layout: {
|
||||||
|
fit: 'contain' as const,
|
||||||
|
position: { x: 1, y: 2 },
|
||||||
|
view_box: { top: 3, bottom: 4, left: 5, right: 6 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
controller.setCameraConfig(config);
|
||||||
|
controller.setContainer(container);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
host.style.getPropertyValue('--advanced-camera-card-media-layout-fit'),
|
||||||
|
).toBe('contain');
|
||||||
|
expect(
|
||||||
|
host.style.getPropertyValue('--advanced-camera-card-media-layout-position-x'),
|
||||||
|
).toBe('1%');
|
||||||
|
expect(
|
||||||
|
host.style.getPropertyValue('--advanced-camera-card-media-layout-position-y'),
|
||||||
|
).toBe('2%');
|
||||||
|
expect(
|
||||||
|
host.style.getPropertyValue('--advanced-camera-card-media-layout-view-box-top'),
|
||||||
|
).toBe('3%');
|
||||||
|
expect(
|
||||||
|
host.style.getPropertyValue(
|
||||||
|
'--advanced-camera-card-media-layout-view-box-bottom',
|
||||||
|
),
|
||||||
|
).toBe('4%');
|
||||||
|
expect(
|
||||||
|
host.style.getPropertyValue('--advanced-camera-card-media-layout-view-box-left'),
|
||||||
|
).toBe('5%');
|
||||||
|
expect(
|
||||||
|
host.style.getPropertyValue(
|
||||||
|
'--advanced-camera-card-media-layout-view-box-right',
|
||||||
|
),
|
||||||
|
).toBe('6%');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore multiple calls to set same container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
|
||||||
|
controller.setContainer(container);
|
||||||
|
|
||||||
|
expect(host.getAttribute('size')).toBe('unsized');
|
||||||
|
host.setAttribute('size', 'sized');
|
||||||
|
|
||||||
|
controller.setContainer(container);
|
||||||
|
expect(host.getAttribute('size')).toBe('sized');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
|
||||||
|
controller.setContainer(container);
|
||||||
|
controller.setContainer();
|
||||||
|
|
||||||
|
expect(host.getAttribute('size')).toBe('unsized');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should set size attribute correctly', () => {
|
||||||
|
it('should set unsized-landscape', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
const config = { aspect_ratio: [16, 9] };
|
||||||
|
|
||||||
|
controller.setCameraConfig(config);
|
||||||
|
controller.setContainer(container);
|
||||||
|
|
||||||
|
expect(container.style.aspectRatio).toBe('16 / 9');
|
||||||
|
expect(host.getAttribute('size')).toBe('unsized-landscape');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set unsized-portrait', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
const config = { aspect_ratio: [9, 16] };
|
||||||
|
|
||||||
|
controller.setCameraConfig(config);
|
||||||
|
controller.setContainer(container);
|
||||||
|
|
||||||
|
expect(container.style.aspectRatio).toBe('9 / 16');
|
||||||
|
expect(host.getAttribute('size')).toBe('unsized-portrait');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set unsized', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
|
||||||
|
controller.setContainer(container);
|
||||||
|
expect(host.getAttribute('size')).toBe('unsized');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set unsized without a config', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
|
||||||
|
controller.setCameraConfig();
|
||||||
|
controller.setContainer(container);
|
||||||
|
|
||||||
|
expect(host.getAttribute('size')).toBe('unsized');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should respond to size changes', () => {
|
||||||
|
it('should set host to unsized if no container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.setAttribute('size', 'sized');
|
||||||
|
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 600,
|
||||||
|
width: 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
new MediaProviderDimensionsController(host);
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
expect(host.getAttribute('size')).toBe('unsized');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set host to unsized if container has no dimensions', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.setAttribute('size', 'sized');
|
||||||
|
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 100,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
controller.setContainer(container);
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
expect(host.getAttribute('size')).toBe('unsized');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore resize calls where actual equals intended size', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.setAttribute('size', 'sized');
|
||||||
|
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 100,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 100,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
controller.setContainer(container);
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
host.setAttribute('size', '__RANDOM__');
|
||||||
|
|
||||||
|
// 2nd call should be ignored.
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
expect(host.getAttribute('size')).toBe('__RANDOM__');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should resize container to fit width-limited container', () => {
|
||||||
|
it('should resize container to fit width-limited container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 200,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 90,
|
||||||
|
width: 160,
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
controller.setContainer(container);
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
expect(container.style.width).toBe('100%');
|
||||||
|
expect(container.style.height).toBe('auto');
|
||||||
|
expect(host.getAttribute('size')).toBe('sized');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resize container to fit height-limited container', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 100,
|
||||||
|
width: 400,
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 200,
|
||||||
|
width: 160,
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
controller.setContainer(container);
|
||||||
|
|
||||||
|
callResizeHandler();
|
||||||
|
|
||||||
|
expect(container.style.width).toBe(`${100 * (160 / 200)}px`);
|
||||||
|
expect(container.style.height).toBe('100px');
|
||||||
|
expect(host.getAttribute('size')).toBe('sized');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should respond to media loading', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(
|
||||||
|
requestAnimationFrameMock,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.mocked(window.requestAnimationFrame).mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resize container after a media load', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
host.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 100,
|
||||||
|
width: 400,
|
||||||
|
});
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
|
height: 90,
|
||||||
|
width: 160,
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new MediaProviderDimensionsController(host);
|
||||||
|
controller.setContainer(container);
|
||||||
|
|
||||||
|
controller.hostConnected();
|
||||||
|
|
||||||
|
dispatchExistingMediaLoadedInfoAsEvent(host, createMediaLoadedInfo());
|
||||||
|
|
||||||
|
expect(container.style.width).toBe(`100%`);
|
||||||
|
expect(container.style.height).toBe('auto');
|
||||||
|
expect(host.getAttribute('size')).toBe('sized');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -486,6 +486,43 @@ export const callVisibilityHandler = async (visible: boolean): Promise<void> =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getResizeObserver = (n = 0): ResizeObserver | null => {
|
||||||
|
const mockResult = vi.mocked(ResizeObserver).mock.results[n];
|
||||||
|
if (mockResult.type !== 'return') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return mockResult.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const callResizeHandler = (
|
||||||
|
entries: {
|
||||||
|
target: HTMLElement;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}[] = [],
|
||||||
|
n = 0,
|
||||||
|
): void => {
|
||||||
|
const observer = getResizeObserver(n);
|
||||||
|
if (!observer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vi.mocked(ResizeObserver).mock.calls[n][0](
|
||||||
|
// Note this is a very incomplete / invalid ResizeObserverEntry that
|
||||||
|
// just provides the bare basics current implementation uses.
|
||||||
|
entries.map(
|
||||||
|
(entry) =>
|
||||||
|
({
|
||||||
|
target: entry.target,
|
||||||
|
contentRect: {
|
||||||
|
height: entry.height,
|
||||||
|
width: entry.width,
|
||||||
|
},
|
||||||
|
}) as unknown as ResizeObserverEntry,
|
||||||
|
),
|
||||||
|
observer,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const createSlotHost = (options?: {
|
export const createSlotHost = (options?: {
|
||||||
slot?: HTMLSlotElement;
|
slot?: HTMLSlotElement;
|
||||||
children?: HTMLElement[];
|
children?: HTMLElement[];
|
||||||
|
|||||||
@@ -35,36 +35,6 @@ export const callEmblaHandler = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const callResizeHandler = (
|
|
||||||
entries: {
|
|
||||||
target: HTMLElement;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}[],
|
|
||||||
n = 0,
|
|
||||||
): void => {
|
|
||||||
const mockResult = vi.mocked(ResizeObserver).mock.results[n];
|
|
||||||
if (mockResult.type !== 'return') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const observer = mockResult.value;
|
|
||||||
vi.mocked(ResizeObserver).mock.calls[n][0](
|
|
||||||
// Note this is a very incomplete / invalid ResizeObserverEntry that
|
|
||||||
// just provides the bare basics current implementation uses.
|
|
||||||
entries.map(
|
|
||||||
(entry) =>
|
|
||||||
({
|
|
||||||
target: entry.target,
|
|
||||||
contentRect: {
|
|
||||||
height: entry.height,
|
|
||||||
width: entry.width,
|
|
||||||
},
|
|
||||||
}) as unknown as ResizeObserverEntry,
|
|
||||||
),
|
|
||||||
observer,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createEmblaApiInstance = (options?: {
|
export const createEmblaApiInstance = (options?: {
|
||||||
slideNodes?: HTMLElement[];
|
slideNodes?: HTMLElement[];
|
||||||
selectedScrollSnap?: number;
|
selectedScrollSnap?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user