diff --git a/src/components/live/carousel.ts b/src/components/live/carousel.ts
index 0f6b6d65..bd749399 100644
--- a/src/components/live/carousel.ts
+++ b/src/components/live/carousel.ts
@@ -38,6 +38,7 @@ import AutoMediaLoadedInfo from '../../utils/embla/plugins/auto-media-loaded-inf
import AutoSize from '../../utils/embla/plugins/auto-size/auto-size.js';
import { getStreamCameraID } from '../../utils/substream.js';
import { View } from '../../view/view.js';
+import '../carousel';
import { EmblaCarouselPlugins } from '../carousel.js';
import { dispatchFrigateCardErrorEvent } from '../message.js';
import '../next-prev-control.js';
diff --git a/src/components/viewer.ts b/src/components/viewer.ts
deleted file mode 100644
index 0af40d63..00000000
--- a/src/components/viewer.ts
+++ /dev/null
@@ -1,969 +0,0 @@
-import {
- CSSResultGroup,
- html,
- LitElement,
- PropertyValues,
- TemplateResult,
- unsafeCSS,
-} from 'lit';
-import { customElement, property, state } from 'lit/decorators.js';
-import { guard } from 'lit/directives/guard.js';
-import { ifDefined } from 'lit/directives/if-defined.js';
-import { createRef, Ref, ref } from 'lit/directives/ref.js';
-import { CameraManager } from '../camera-manager/manager.js';
-import { RemoveContextPropertyViewModifier } from '../card-controller/view/modifiers/remove-context-property.js';
-import { ViewManagerEpoch } from '../card-controller/view/types.js';
-import { MediaActionsController } from '../components-lib/media-actions-controller.js';
-import { MediaGridSelected } from '../components-lib/media-grid-controller.js';
-import { ZoomSettingsObserved } from '../components-lib/zoom/types.js';
-import { handleZoomSettingsObservedEvent } from '../components-lib/zoom/zoom-view-context.js';
-import {
- dispatchMessageEvent,
- renderMessage,
- renderProgressIndicator,
-} from '../components/message.js';
-import {
- CardWideConfig,
- frigateCardConfigDefaults,
- TransitionEffect,
- ViewerConfig,
-} from '../config/types.js';
-import { localize } from '../localize/localize.js';
-import '../patches/ha-hls-player';
-import basicBlockStyle from '../scss/basic-block.scss';
-import viewerCarouselStyle from '../scss/viewer-carousel.scss';
-import viewerProviderStyle from '../scss/viewer-provider.scss';
-import viewerStyle from '../scss/viewer.scss';
-import {
- ExtendedHomeAssistant,
- FrigateCardMediaPlayer,
- MediaLoadedInfo,
-} from '../types.js';
-import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
-import { mayHaveAudio } from '../utils/audio.js';
-import {
- aspectRatioToString,
- contentsChanged,
- setOrRemoveAttribute,
-} from '../utils/basic.js';
-import { CarouselSelected } from '../utils/embla/carousel-controller.js';
-import { AutoLazyLoad } from '../utils/embla/plugins/auto-lazy-load/auto-lazy-load.js';
-import AutoMediaLoadedInfo from '../utils/embla/plugins/auto-media-loaded-info/auto-media-loaded-info.js';
-import AutoSize from '../utils/embla/plugins/auto-size/auto-size.js';
-import { canonicalizeHAURL } from '../utils/ha/index.js';
-import { ResolvedMediaCache, resolveMedia } from '../utils/ha/resolved-media.js';
-import {
- dispatchMediaLoadedEvent,
- dispatchMediaPauseEvent,
- dispatchMediaPlayEvent,
- dispatchMediaVolumeChangeEvent,
-} from '../utils/media-info.js';
-import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
-import {
- hideMediaControlsTemporarily,
- MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
- playMediaMutingIfNecessary,
- setControlsOnVideo,
-} from '../utils/media.js';
-import { screenshotMedia } from '../utils/screenshot.js';
-import { ViewMediaClassifier } from '../view/media-classifier';
-import { MediaQueriesClassifier } from '../view/media-queries-classifier';
-import { VideoContentType, ViewMedia } from '../view/media.js';
-import type { EmblaCarouselPlugins } from './carousel.js';
-import './next-prev-control.js';
-import './ptz';
-import './surround.js';
-
-export interface MediaViewerViewContext {
- seek?: Date;
-}
-
-declare module 'view' {
- interface ViewContext {
- mediaViewer?: MediaViewerViewContext;
- }
-}
-
-interface MediaNeighbor {
- index: number;
- media: ViewMedia;
-}
-
-interface MediaNeighbors {
- previous?: MediaNeighbor;
- next?: MediaNeighbor;
-}
-
-@customElement('frigate-card-viewer')
-export class FrigateCardViewer extends LitElement {
- @property({ attribute: false })
- public hass?: ExtendedHomeAssistant;
-
- @property({ attribute: false })
- public viewManagerEpoch?: ViewManagerEpoch;
-
- @property({ attribute: false })
- public viewerConfig?: ViewerConfig;
-
- @property({ attribute: false })
- public resolvedMediaCache?: ResolvedMediaCache;
-
- @property({ attribute: false })
- public cameraManager?: CameraManager;
-
- @property({ attribute: false })
- public cardWideConfig?: CardWideConfig;
-
- protected render(): TemplateResult | void {
- if (
- !this.hass ||
- !this.viewManagerEpoch ||
- !this.viewerConfig ||
- !this.cameraManager ||
- !this.cardWideConfig
- ) {
- return;
- }
-
- if (!this.viewManagerEpoch.manager.getView()?.queryResults?.hasResults()) {
- // Directly render an error message (instead of dispatching it upwards)
- // to preserve the mini-timeline if the user pans into an area with no
- // media.
- return renderMessage({
- type: 'info',
- message: localize('common.no_media'),
- icon: 'mdi:multimedia',
- });
- }
-
- return html`
- `;
- }
-
- static get styles(): CSSResultGroup {
- return unsafeCSS(viewerStyle);
- }
-}
-
-const FRIGATE_CARD_VIEWER_PROVIDER = 'frigate-card-viewer-provider';
-
-@customElement('frigate-card-viewer-carousel')
-export class FrigateCardViewerCarousel extends LitElement {
- @property({ attribute: false })
- public hass?: ExtendedHomeAssistant;
-
- @property({ attribute: false })
- public viewManagerEpoch?: ViewManagerEpoch;
-
- @property({ attribute: false })
- public viewFilterCameraID?: string;
-
- // Resetting the viewer configuration causes a full reset so ensure the config
- // has actually changed with a full comparison (dynamic configuration
- // overrides may causes changes elsewhere in the full card configuration that
- // could lead to the address of the viewerConfig changing without it being
- // semantically different).
- @property({ attribute: false, hasChanged: contentsChanged })
- public viewerConfig?: ViewerConfig;
-
- @property({ attribute: false })
- public resolvedMediaCache?: ResolvedMediaCache;
-
- @property({ attribute: false })
- public cardWideConfig?: CardWideConfig;
-
- @property({ attribute: false })
- public cameraManager?: CameraManager;
-
- @property({ attribute: false })
- public showControls = true;
-
- @state()
- protected _selected = 0;
-
- protected _media: ViewMedia[] | null = null;
- protected _mediaActionsController = new MediaActionsController();
- protected _player: FrigateCardMediaPlayer | null = null;
- protected _refCarousel: Ref = createRef();
-
- updated(changedProperties: PropertyValues): void {
- super.updated(changedProperties);
-
- if (changedProperties.has('viewManagerEpoch')) {
- // Seek into the video if the seek time has changed (this is also called
- // on media load, since the media may or may not have been loaded at
- // this point).
- if (
- this.viewManagerEpoch?.manager.getView()?.context?.mediaViewer !==
- this.viewManagerEpoch?.oldView?.context?.mediaViewer
- ) {
- this._seekHandler();
- }
- }
-
- if (!this._mediaActionsController.hasRoot() && this._refCarousel.value) {
- this._mediaActionsController.initialize(this._refCarousel.value);
- }
- }
-
- public connectedCallback(): void {
- super.connectedCallback();
-
- // Request update in order to reinitialize the media action controller.
- this.requestUpdate();
- }
-
- public disconnectedCallback(): void {
- this._mediaActionsController.destroy();
- super.disconnectedCallback();
- }
-
- /**
- * Get the transition effect to use.
- * @returns An TransitionEffect object.
- */
- protected _getTransitionEffect(): TransitionEffect {
- return (
- this.viewerConfig?.transition_effect ??
- frigateCardConfigDefaults.media_viewer.transition_effect
- );
- }
-
- /**
- * Get the Embla plugins to use.
- * @returns A list of EmblaOptionsTypes.
- */
- protected _getPlugins(): EmblaCarouselPlugins {
- return [
- AutoLazyLoad({
- ...(this.viewerConfig?.lazy_load && {
- lazyLoadCallback: (_index, slide) => this._lazyloadSlide(slide),
- }),
- }),
- AutoMediaLoadedInfo(),
- AutoSize(),
- ];
- }
-
- /**
- * Get the previous and next true media items from the current view.
- * @returns A BrowseMediaNeighbors with indices and objects of true media
- * neighbors.
- */
- protected _getMediaNeighbors(): MediaNeighbors | null {
- const mediaCount = this._media?.length ?? 0;
- if (!this._media) {
- return null;
- }
-
- const prevIndex = this._selected > 0 ? this._selected - 1 : null;
- const nextIndex = this._selected + 1 < mediaCount ? this._selected + 1 : null;
- return {
- ...(prevIndex !== null && {
- previous: {
- index: prevIndex,
- media: this._media[prevIndex],
- },
- }),
- ...(nextIndex !== null && {
- next: {
- index: nextIndex,
- media: this._media[nextIndex],
- },
- }),
- };
- }
-
- protected _setViewSelectedIndex(index: number): void {
- const view = this.viewManagerEpoch?.manager.getView();
-
- if (!this._media || !view) {
- return;
- }
-
- if (this._selected === index) {
- // The slide may already be selected on load, so don't dispatch a new view
- // unless necessary (i.e. the new index is different from the current
- // index).
- return;
- }
-
- const newResults = view?.queryResults
- ?.clone()
- .selectIndex(index, this.viewFilterCameraID);
- if (!newResults) {
- return;
- }
- const cameraID = newResults
- .getSelectedResult(this.viewFilterCameraID)
- ?.getCameraID();
-
- this.viewManagerEpoch?.manager.setViewByParameters({
- params: {
- queryResults: newResults,
- // Always change the camera to the owner of the selected media.
- ...(cameraID && { camera: cameraID }),
- },
- modifiers: [new RemoveContextPropertyViewModifier('mediaViewer', 'seek')],
- });
- }
-
- /**
- * Lazy load a slide.
- * @param slide The slide to lazy load.
- */
- protected _lazyloadSlide(slide: Element): void {
- if (slide instanceof HTMLSlotElement) {
- slide = slide.assignedElements({ flatten: true })[0];
- }
-
- const viewerProvider = slide?.querySelector(
- 'frigate-card-viewer-provider',
- ) as FrigateCardViewerProvider | null;
- if (viewerProvider) {
- viewerProvider.load = true;
- }
- }
-
- /**
- * Get slides to include in the render.
- * @returns The slides to include in the render.
- */
- protected _getSlides(): TemplateResult[] {
- if (!this._media) {
- return [];
- }
-
- const slides: TemplateResult[] = [];
- for (let i = 0; i < this._media.length; ++i) {
- const media = this._media[i];
- if (media) {
- const slide = this._renderMediaItem(media);
- if (slide) {
- slides[i] = slide;
- }
- }
- }
- return slides;
- }
-
- protected willUpdate(changedProps: PropertyValues): void {
- if (changedProps.has('viewerConfig')) {
- this._mediaActionsController.setOptions({
- playerSelector: FRIGATE_CARD_VIEWER_PROVIDER,
- ...(this.viewerConfig?.auto_play && {
- autoPlayConditions: this.viewerConfig.auto_play,
- }),
- ...(this.viewerConfig?.auto_pause && {
- autoPauseConditions: this.viewerConfig.auto_pause,
- }),
- ...(this.viewerConfig?.auto_mute && {
- autoMuteConditions: this.viewerConfig.auto_mute,
- }),
- ...(this.viewerConfig?.auto_unmute && {
- autoUnmuteConditions: this.viewerConfig.auto_unmute,
- }),
- });
- }
-
- if (changedProps.has('viewManagerEpoch')) {
- const view = this.viewManagerEpoch?.manager.getView();
- const newMedia = view?.queryResults?.getResults(this.viewFilterCameraID) ?? null;
- const newSelected =
- view?.queryResults?.getSelectedIndex(this.viewFilterCameraID) ?? 0;
- const newSeek = view?.context?.mediaViewer?.seek;
-
- if (newMedia !== this._media || newSelected !== this._selected || !newSeek) {
- setOrRemoveAttribute(this, false, 'unseekable');
- this._media = newMedia;
- this._selected = newSelected;
- }
-
- if (!newMedia?.length) {
- // No media will be rendered.
- this._mediaActionsController.unsetTarget();
- } else {
- if (this.viewFilterCameraID) {
- this._mediaActionsController.setTarget(
- newSelected,
- // Camera in this carousel is only selected if the camera from the
- // view matches the filtered camera.
- view?.camera === this.viewFilterCameraID,
- );
- } else {
- // Carousel is not filtered, so the targeted camera is always selected.
- this._mediaActionsController.setTarget(newSelected, true);
- }
- }
- }
- }
-
- protected render(): TemplateResult | void {
- const mediaCount = this._media?.length ?? 0;
- if (!this._media || !mediaCount) {
- return dispatchMessageEvent(this, localize('common.no_media'), 'info', {
- icon: 'mdi:multimedia',
- });
- }
-
- // If there's no selected media, just choose the last (most recent one) to
- // avoid rendering a blank. This situation should not occur in practice, as
- // this view should not be called without a selected media.
- const selectedMedia = this._media[this._selected] ?? this._media[mediaCount - 1];
-
- if (!this.hass || !this.cameraManager || !selectedMedia) {
- return;
- }
-
- const neighbors = this._getMediaNeighbors();
- const scroll = (direction: 'previous' | 'next'): void => {
- if (!neighbors || !this._media) {
- return;
- }
- const newIndex =
- (direction === 'previous' ? neighbors.previous?.index : neighbors.next?.index) ??
- null;
- if (newIndex !== null) {
- this._setViewSelectedIndex(newIndex);
- }
- };
-
- const view = this.viewManagerEpoch?.manager.getView();
-
- return html`
- ) => {
- this._setViewSelectedIndex(ev.detail.index);
- }}
- @frigate-card:media:loaded=${(ev: CustomEvent) => {
- this._player = ev.detail.player ?? null;
- this._seekHandler();
- }}
- @frigate-card:media:unloaded=${() => {
- this._player = null;
- }}
- >
- ${this.showControls
- ? html` {
- scroll('previous');
- stopEventFromActivatingCardWideActions(ev);
- }}
- >`
- : ''}
- ${guard([this._media, view], () => this._getSlides())}
- ${this.showControls
- ? html` {
- scroll('next');
- stopEventFromActivatingCardWideActions(ev);
- }}
- >`
- : ''}
-
- ${view
- ? html`
- `
- : ''}
-
-
-
-
- `;
- }
-
- /**
- * Fire a media show event when a slide is selected.
- */
- protected async _seekHandler(): Promise {
- const view = this.viewManagerEpoch?.manager.getView();
- const seek = view?.context?.mediaViewer?.seek;
- if (!this.hass || !seek || !this._media || !this._player) {
- return;
- }
- const selectedMedia = this._media[this._selected];
- if (!selectedMedia) {
- return;
- }
-
- const seekTimeInMedia = selectedMedia.includesTime(seek);
- setOrRemoveAttribute(this, !seekTimeInMedia, 'unseekable');
- if (!seekTimeInMedia && !this._player.isPaused()) {
- this._player.pause();
- } else if (seekTimeInMedia && this._player.isPaused()) {
- this._player.play();
- }
-
- const seekTime =
- (await this.cameraManager?.getMediaSeekTime(selectedMedia, seek)) ?? null;
-
- if (seekTime !== null) {
- this._player.seek(seekTime);
- }
- }
-
- protected _renderMediaItem(media: ViewMedia): TemplateResult | null {
- const view = this.viewManagerEpoch?.manager.getView();
- if (!this.hass || !view || !this.viewerConfig) {
- return null;
- }
-
- return html`
-
-
`;
- }
-
- static get styles(): CSSResultGroup {
- return unsafeCSS(viewerCarouselStyle);
- }
-}
-
-@customElement('frigate-card-viewer-grid')
-export class FrigateCardViewerGrid extends LitElement {
- @property({ attribute: false })
- public hass?: ExtendedHomeAssistant;
-
- @property({ attribute: false })
- public viewManagerEpoch?: ViewManagerEpoch;
-
- @property({ attribute: false })
- public viewerConfig?: ViewerConfig;
-
- @property({ attribute: false })
- public resolvedMediaCache?: ResolvedMediaCache;
-
- @property({ attribute: false })
- public cardWideConfig?: CardWideConfig;
-
- @property({ attribute: false })
- public cameraManager?: CameraManager;
-
- protected _renderCarousel(filterCamera?: string): TemplateResult {
- const selectedCameraID = this.viewManagerEpoch?.manager.getView()?.camera;
- return html`
-
-
- `;
- }
-
- protected willUpdate(changedProps: PropertyValues): void {
- if (changedProps.has('viewManagerEpoch') && this._needsGrid()) {
- import('./media-grid.js');
- }
- }
-
- protected _needsGrid(): boolean {
- const view = this.viewManagerEpoch?.manager.getView();
- const cameraIDs = view?.queryResults?.getCameraIDs();
- return (
- !!view?.isGrid() &&
- !!view?.supportsMultipleDisplayModes() &&
- (cameraIDs?.size ?? 0) > 1
- );
- }
-
- protected _gridSelectCamera(cameraID: string): void {
- const view = this.viewManagerEpoch?.manager.getView();
- this.viewManagerEpoch?.manager.setViewByParameters({
- params: {
- camera: cameraID,
- queryResults: view?.queryResults
- ?.clone()
- .promoteCameraSelectionToMainSelection(cameraID),
- },
- });
- }
-
- protected render(): TemplateResult {
- const view = this.viewManagerEpoch?.manager.getView();
- const cameraIDs = view?.queryResults?.getCameraIDs();
- if (!cameraIDs || !this._needsGrid()) {
- return this._renderCarousel();
- }
-
- return html`
- ) =>
- this._gridSelectCamera(ev.detail.selected)}
- >
- ${[...cameraIDs].map((cameraID) => this._renderCarousel(cameraID))}
-
- `;
- }
-
- static get styles(): CSSResultGroup {
- return unsafeCSS(basicBlockStyle);
- }
-}
-
-@customElement(FRIGATE_CARD_VIEWER_PROVIDER)
-export class FrigateCardViewerProvider
- extends LitElement
- implements FrigateCardMediaPlayer
-{
- @property({ attribute: false })
- public hass?: ExtendedHomeAssistant;
-
- @property({ attribute: false })
- public viewManagerEpoch?: ViewManagerEpoch;
-
- @property({ attribute: false })
- public media?: ViewMedia;
-
- @property({ attribute: false })
- public viewerConfig?: ViewerConfig;
-
- @property({ attribute: false })
- public resolvedMediaCache?: ResolvedMediaCache;
-
- // Whether or not to load the viewer media. If `false`, no contents are
- // rendered until this attribute is set to `true` (this is useful for lazy
- // loading).
- @property({ attribute: false })
- public load = false;
-
- @property({ attribute: false })
- public cameraManager?: CameraManager;
-
- @property({ attribute: false })
- public cardWideConfig?: CardWideConfig;
-
- protected _refFrigateCardMediaPlayer: Ref =
- createRef();
- protected _refVideoProvider: Ref = createRef();
- protected _refImageProvider: Ref = createRef();
-
- public async play(): Promise {
- await playMediaMutingIfNecessary(
- this,
- this._refFrigateCardMediaPlayer.value ?? this._refVideoProvider.value,
- );
- }
-
- public async pause(): Promise {
- (this._refFrigateCardMediaPlayer.value || this._refVideoProvider.value)?.pause();
- }
-
- public async mute(): Promise {
- if (this._refFrigateCardMediaPlayer.value) {
- this._refFrigateCardMediaPlayer.value?.mute();
- } else if (this._refVideoProvider.value) {
- this._refVideoProvider.value.muted = true;
- }
- }
-
- public async unmute(): Promise {
- if (this._refFrigateCardMediaPlayer.value) {
- this._refFrigateCardMediaPlayer.value?.mute();
- } else if (this._refVideoProvider.value) {
- this._refVideoProvider.value.muted = false;
- }
- }
-
- public isMuted(): boolean {
- if (this._refFrigateCardMediaPlayer.value) {
- return this._refFrigateCardMediaPlayer.value?.isMuted() ?? true;
- } else if (this._refVideoProvider.value) {
- return this._refVideoProvider.value.muted;
- }
- return true;
- }
-
- public async seek(seconds: number): Promise {
- if (this._refFrigateCardMediaPlayer.value) {
- return this._refFrigateCardMediaPlayer.value.seek(seconds);
- } else if (this._refVideoProvider.value) {
- hideMediaControlsTemporarily(this._refVideoProvider.value);
- this._refVideoProvider.value.currentTime = seconds;
- }
- }
-
- public async setControls(controls?: boolean): Promise {
- if (this._refFrigateCardMediaPlayer.value) {
- return this._refFrigateCardMediaPlayer.value.setControls(controls);
- } else if (this._refVideoProvider.value) {
- setControlsOnVideo(
- this._refVideoProvider.value,
- controls ?? this.viewerConfig?.controls.builtin ?? true,
- );
- }
- }
-
- public isPaused(): boolean {
- if (this._refFrigateCardMediaPlayer.value) {
- return this._refFrigateCardMediaPlayer.value.isPaused();
- } else if (this._refVideoProvider.value) {
- return this._refVideoProvider.value.paused;
- }
- return true;
- }
-
- public async getScreenshotURL(): Promise {
- if (this._refFrigateCardMediaPlayer.value) {
- return await this._refFrigateCardMediaPlayer.value.getScreenshotURL();
- } else if (this._refVideoProvider.value) {
- return screenshotMedia(this._refVideoProvider.value);
- } else if (this._refImageProvider.value) {
- return this._refImageProvider.value.src;
- }
- return null;
- }
-
- protected async _switchToRelatedClipView(): Promise {
- const view = this.viewManagerEpoch?.manager.getView();
- if (
- !this.hass ||
- !view ||
- !this.cameraManager ||
- !this.media ||
- // If this specific media item has no clip, then do nothing (even if all
- // the other media items do).
- !ViewMediaClassifier.isEvent(this.media) ||
- !MediaQueriesClassifier.areEventQueries(view.query)
- ) {
- return;
- }
-
- // Convert the query to a clips equivalent.
- const clipQuery = view.query.clone();
- clipQuery.convertToClipsQueries();
-
- const queries = clipQuery.getQueries();
- if (!queries) {
- return;
- }
-
- await this.viewManagerEpoch?.manager.setViewByParametersWithExistingQuery({
- params: {
- view: 'media',
- query: clipQuery,
- },
- queryExecutorOptions: {
- selectResult: {
- id: this.media.getID() ?? undefined,
- },
- rejectResults: (results) => !results.hasSelectedResult(),
- },
- });
- }
-
- protected willUpdate(changedProps: PropertyValues): void {
- const mediaContentID = this.media ? this.media.getContentID() : null;
-
- if (
- (changedProps.has('load') ||
- changedProps.has('media') ||
- changedProps.has('viewerConfig') ||
- changedProps.has('resolvedMediaCache') ||
- changedProps.has('hass')) &&
- this.hass &&
- mediaContentID &&
- !this.resolvedMediaCache?.has(mediaContentID) &&
- (!this.viewerConfig?.lazy_load || this.load)
- ) {
- resolveMedia(this.hass, mediaContentID, this.resolvedMediaCache).then(() => {
- this.requestUpdate();
- });
- }
-
- if (changedProps.has('viewerConfig') && this.viewerConfig?.zoomable) {
- import('./zoomer.js');
- }
-
- if (changedProps.has('media') || changedProps.has('cameraManager')) {
- const cameraID = this.media?.getCameraID();
- const cameraConfig = cameraID
- ? this.cameraManager?.getStore().getCameraConfig(cameraID)
- : null;
- updateElementStyleFromMediaLayoutConfig(this, cameraConfig?.dimensions?.layout);
-
- this.style.aspectRatio = aspectRatioToString({
- ratio: cameraConfig?.dimensions?.aspect_ratio,
- });
- }
- }
-
- protected _useZoomIfRequired(template: TemplateResult): TemplateResult {
- if (!this.media) {
- return template;
- }
- const cameraID = this.media.getCameraID();
- const mediaID = this.media.getID() ?? undefined;
- const cameraConfig = this.cameraManager?.getStore().getCameraConfig(cameraID);
- const view = this.viewManagerEpoch?.manager.getView();
-
- return this.viewerConfig?.zoomable
- ? html`
- cameraConfig?.dimensions?.layout
- ? {
- pan: cameraConfig.dimensions.layout.pan,
- zoom: cameraConfig.dimensions.layout.zoom,
- }
- : undefined,
- )}
- .settings=${mediaID ? view?.context?.zoom?.[mediaID]?.requested : undefined}
- @frigate-card:zoom:zoomed=${() => this.setControls(false)}
- @frigate-card:zoom:unzoomed=${() => this.setControls()}
- @frigate-card:zoom:change=${(ev: CustomEvent) =>
- handleZoomSettingsObservedEvent(ev, this.viewManagerEpoch?.manager, mediaID)}
- >
- ${template}
- `
- : template;
- }
-
- protected render(): TemplateResult | void {
- if (!this.load || !this.media || !this.hass || !this.viewerConfig) {
- return;
- }
-
- const mediaContentID = this.media.getContentID();
- const resolvedMedia = mediaContentID
- ? this.resolvedMediaCache?.get(mediaContentID)
- : null;
- if (!resolvedMedia) {
- // Media will be resolved with the call in willUpdate() then this will be
- // re-rendered.
- return renderProgressIndicator({
- cardWideConfig: this.cardWideConfig,
- });
- }
-
- // Note: crossorigin="anonymous" is required on