Refactor views to support dynamic updates.

This commit is contained in:
Dermot Duffy
2024-07-26 19:15:38 -07:00
parent 33dee47205
commit d5c4e56c45
80 changed files with 3714 additions and 4021 deletions
+120 -167
View File
@@ -11,6 +11,8 @@ 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 { 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';
@@ -41,7 +43,6 @@ import { mayHaveAudio } from '../utils/audio.js';
import {
aspectRatioToString,
contentsChanged,
errorToConsole,
setOrRemoveAttribute,
} from '../utils/basic.js';
import { CarouselSelected } from '../utils/embla/carousel-controller.js';
@@ -58,10 +59,6 @@ import {
dispatchMediaVolumeChangeEvent,
} from '../utils/media-info.js';
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
import {
changeViewToRecentEventsForCameraAndDependents,
changeViewToRecentRecordingForCameraAndDependents,
} from '../utils/media-to-view.js';
import {
hideMediaControlsTemporarily,
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
@@ -71,9 +68,7 @@ import {
import { screenshotMedia } from '../utils/screenshot.js';
import { ViewMediaClassifier } from '../view/media-classifier';
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
import { MediaQueriesResults } from '../view/media-queries-results.js';
import { VideoContentType, ViewMedia } from '../view/media.js';
import { View } from '../view/view.js';
import type { EmblaCarouselPlugins } from './carousel.js';
import './next-prev-control.js';
import './ptz';
@@ -110,7 +105,7 @@ export class FrigateCardViewer extends LitElement {
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public view?: Readonly<View>;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false })
public viewerConfig?: ViewerConfig;
@@ -127,7 +122,7 @@ export class FrigateCardViewer extends LitElement {
protected render(): TemplateResult | void {
if (
!this.hass ||
!this.view ||
!this.viewManagerEpoch ||
!this.viewerConfig ||
!this.cameraManager ||
!this.cardWideConfig
@@ -135,55 +130,20 @@ export class FrigateCardViewer extends LitElement {
return;
}
if (!this.view.queryResults?.hasResults()) {
// If the query is not specified, the view must tell us which mediaType to
// search for. When the query *is* specified, the view is not required to
// indicate the media type (e.g. the mixed 'media' view from the
// timeline).
const mediaType = this.view.getDefaultMediaType();
if (!mediaType) {
// 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',
});
}
if (mediaType === 'recordings') {
changeViewToRecentRecordingForCameraAndDependents(
this,
this.cameraManager,
this.cardWideConfig,
this.view,
{
allCameras: this.view.isGrid(),
targetView: 'recording',
useCache: false,
},
);
} else {
changeViewToRecentEventsForCameraAndDependents(
this,
this.cameraManager,
this.cardWideConfig,
this.view,
{
allCameras: this.view.isGrid(),
targetView: 'media',
eventsMediaType: mediaType,
useCache: false,
},
);
}
return renderProgressIndicator({ cardWideConfig: this.cardWideConfig });
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` <frigate-card-viewer-grid
.hass=${this.hass}
.view=${this.view}
.viewManagerEpoch=${this.viewManagerEpoch}
.viewerConfig=${this.viewerConfig}
.resolvedMediaCache=${this.resolvedMediaCache}
.cameraManager=${this.cameraManager}
@@ -205,7 +165,7 @@ export class FrigateCardViewerCarousel extends LitElement {
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public view?: Readonly<View>;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false })
public viewFilterCameraID?: string;
@@ -227,6 +187,9 @@ export class FrigateCardViewerCarousel extends LitElement {
@property({ attribute: false })
public cameraManager?: CameraManager;
@property({ attribute: false })
public showControls = true;
@state()
protected _selected = 0;
@@ -241,12 +204,14 @@ export class FrigateCardViewerCarousel extends LitElement {
updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has('view')) {
const oldView = changedProperties.get('view') as View | undefined;
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.view?.context?.mediaViewer !== oldView?.context?.mediaViewer) {
if (
this.viewManagerEpoch?.manager.getView()?.context?.mediaViewer !==
this.viewManagerEpoch?.oldView?.context?.mediaViewer
) {
this._seekHandler();
}
}
@@ -324,7 +289,9 @@ export class FrigateCardViewerCarousel extends LitElement {
}
protected _setViewSelectedIndex(index: number): void {
if (!this._media) {
const view = this.viewManagerEpoch?.manager.getView();
if (!this._media || !view) {
return;
}
@@ -335,7 +302,7 @@ export class FrigateCardViewerCarousel extends LitElement {
return;
}
const newResults = this.view?.queryResults
const newResults = view?.queryResults
?.clone()
.selectIndex(index, this.viewFilterCameraID);
if (!newResults) {
@@ -345,15 +312,14 @@ export class FrigateCardViewerCarousel extends LitElement {
.getSelectedResult(this.viewFilterCameraID)
?.getCameraID();
this.view
?.evolve({
this.viewManagerEpoch?.manager.setViewByParameters({
params: {
queryResults: newResults,
// Always change the camera to the owner of the selected media.
...(cameraID && { camera: cameraID }),
})
.removeContextProperty('mediaViewer', 'seek')
.dispatchChangeEvent(this);
},
modifiers: [new RemoveContextPropertyViewModifier('mediaViewer', 'seek')],
});
}
/**
@@ -395,17 +361,13 @@ export class FrigateCardViewerCarousel extends LitElement {
return slides;
}
/**
* Called when an update will occur.
* @param changedProps The changed properties
*/
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('view')) {
const newMedia =
this.view?.queryResults?.getResults(this.viewFilterCameraID) ?? null;
if (changedProps.has('viewManagerEpoch')) {
const view = this.viewManagerEpoch?.manager.getView();
const newMedia = view?.queryResults?.getResults(this.viewFilterCameraID) ?? null;
const newSelected =
this.view?.queryResults?.getSelectedIndex(this.viewFilterCameraID) ?? 0;
const newSeek = this.view?.context?.mediaViewer?.seek;
view?.queryResults?.getSelectedIndex(this.viewFilterCameraID) ?? 0;
const newSeek = view?.context?.mediaViewer?.seek;
if (newMedia !== this._media || newSelected !== this._selected || !newSeek) {
setOrRemoveAttribute(this, false, 'unseekable');
@@ -449,8 +411,9 @@ export class FrigateCardViewerCarousel extends LitElement {
selectedMedia.getCameraID(),
);
const view = this.viewManagerEpoch?.manager.getView();
const titleConfig = getDefaultTitleConfigForView(
this.view,
view,
this.viewerConfig?.controls.title,
);
@@ -474,38 +437,42 @@ export class FrigateCardViewerCarousel extends LitElement {
this._player = null;
}}
>
<frigate-card-next-previous-control
slot="previous"
.hass=${this.hass}
.direction=${'previous'}
.controlConfig=${this.viewerConfig?.controls.next_previous}
.thumbnail=${neighbors?.previous?.media.getThumbnail() ?? undefined}
.label=${neighbors?.previous?.media.getTitle() ?? ''}
?disabled=${!neighbors?.previous}
@click=${(ev: Event) => {
scroll('previous');
stopEventFromActivatingCardWideActions(ev);
}}
></frigate-card-next-previous-control>
${guard([this._media, this.view], () => this._getSlides())}
<frigate-card-next-previous-control
slot="next"
.hass=${this.hass}
.direction=${'next'}
.controlConfig=${this.viewerConfig?.controls.next_previous}
.thumbnail=${neighbors?.next?.media.getThumbnail() ?? undefined}
.label=${neighbors?.next?.media.getTitle() ?? ''}
?disabled=${!neighbors?.next}
@click=${(ev: Event) => {
scroll('next');
stopEventFromActivatingCardWideActions(ev);
}}
></frigate-card-next-previous-control>
${this.showControls
? html` <frigate-card-next-previous-control
slot="previous"
.hass=${this.hass}
.direction=${'previous'}
.controlConfig=${this.viewerConfig?.controls.next_previous}
.thumbnail=${neighbors?.previous?.media.getThumbnail() ?? undefined}
.label=${neighbors?.previous?.media.getTitle() ?? ''}
?disabled=${!neighbors?.previous}
@click=${(ev: Event) => {
scroll('previous');
stopEventFromActivatingCardWideActions(ev);
}}
></frigate-card-next-previous-control>`
: ''}
${guard([this._media, view], () => this._getSlides())}
${this.showControls
? html` <frigate-card-next-previous-control
slot="next"
.hass=${this.hass}
.direction=${'next'}
.controlConfig=${this.viewerConfig?.controls.next_previous}
.thumbnail=${neighbors?.next?.media.getThumbnail() ?? undefined}
.label=${neighbors?.next?.media.getTitle() ?? ''}
?disabled=${!neighbors?.next}
@click=${(ev: Event) => {
scroll('next');
stopEventFromActivatingCardWideActions(ev);
}}
></frigate-card-next-previous-control>`
: ''}
</frigate-card-carousel>
${this.view
${view
? html` <frigate-card-ptz
.config=${this.viewerConfig?.controls.ptz}
.forceVisibility=${this.view?.context?.ptzControls?.enabled}
.forceVisibility=${view?.context?.ptzControls?.enabled}
>
</frigate-card-ptz>`
: ''}
@@ -530,7 +497,8 @@ export class FrigateCardViewerCarousel extends LitElement {
* Fire a media show event when a slide is selected.
*/
protected async _seekHandler(): Promise<void> {
const seek = this.view?.context?.mediaViewer?.seek;
const view = this.viewManagerEpoch?.manager.getView();
const seek = view?.context?.mediaViewer?.seek;
if (!this.hass || !seek || !this._media || !this._player) {
return;
}
@@ -556,14 +524,15 @@ export class FrigateCardViewerCarousel extends LitElement {
}
protected _renderMediaItem(media: ViewMedia): TemplateResult | null {
if (!this.hass || !this.view || !this.viewerConfig) {
const view = this.viewManagerEpoch?.manager.getView();
if (!this.hass || !view || !this.viewerConfig) {
return null;
}
return html` <div class="embla__slide">
<frigate-card-viewer-provider
.hass=${this.hass}
.view=${this.view}
.view=${view}
.media=${media}
.viewerConfig=${this.viewerConfig}
.resolvedMediaCache=${this.resolvedMediaCache}
@@ -585,7 +554,7 @@ export class FrigateCardViewerGrid extends LitElement {
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public view?: Readonly<View>;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false })
public viewerConfig?: ViewerConfig;
@@ -600,66 +569,64 @@ export class FrigateCardViewerGrid extends LitElement {
public cameraManager?: CameraManager;
protected _renderCarousel(filterCamera?: string): TemplateResult {
const selectedCameraID = this.viewManagerEpoch?.manager.getView()?.camera;
return html`
<frigate-card-viewer-carousel
grid-id=${ifDefined(filterCamera)}
.hass=${this.hass}
.view=${this.view}
.viewManagerEpoch=${this.viewManagerEpoch}
.viewFilterCameraID=${filterCamera}
.viewerConfig=${this.viewerConfig}
.resolvedMediaCache=${this.resolvedMediaCache}
.cameraManager=${this.cameraManager}
.cardWideConfig=${this.cardWideConfig}
.showControls=${!filterCamera || selectedCameraID === filterCamera}
>
</frigate-card-viewer-carousel>
`;
}
protected _gridSelectCamera(cameraID: string, view?: View): void {
const newView = view ?? this.view;
const promotedQueryResults = newView?.queryResults
?.clone()
.promoteCameraSelectionToMainSelection(cameraID);
newView
?.evolve({
camera: cameraID,
queryResults: promotedQueryResults,
})
.dispatchChangeEvent(this);
}
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('view') && this._needsGrid()) {
if (changedProps.has('viewManagerEpoch') && this._needsGrid()) {
import('./media-grid.js');
}
}
protected _needsGrid(): boolean {
const cameraIDs = this.view?.queryResults?.getCameraIDs();
const view = this.viewManagerEpoch?.manager.getView();
const cameraIDs = view?.queryResults?.getCameraIDs();
return (
!!this.view?.isGrid() &&
!!this.view?.supportsMultipleDisplayModes() &&
!!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 cameraIDs = this.view?.queryResults?.getCameraIDs();
const view = this.viewManagerEpoch?.manager.getView();
const cameraIDs = view?.queryResults?.getCameraIDs();
if (!cameraIDs || !this._needsGrid()) {
return this._renderCarousel();
}
return html`
<frigate-card-media-grid
.selected=${this.view?.camera}
.selected=${view?.camera}
.displayConfig=${this.viewerConfig?.display}
@frigate-card:media-grid:selected=${(ev: CustomEvent<MediaGridSelected>) =>
this._gridSelectCamera(ev.detail.selected)}
@frigate-card:view:change=${(ev: CustomEvent<View>) => {
ev.stopPropagation();
const childView = ev.detail;
this._gridSelectCamera(childView.camera, childView);
}}
>
${[...cameraIDs].map((cameraID) => this._renderCarousel(cameraID))}
</frigate-card-media-grid>
@@ -680,7 +647,7 @@ export class FrigateCardViewerProvider
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public view?: Readonly<View>;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false })
public media?: ViewMedia;
@@ -788,21 +755,22 @@ export class FrigateCardViewerProvider
* Dispatch a clip view that matches the current (snapshot) query.
*/
protected async _dispatchRelatedClipView(): Promise<void> {
const view = this.viewManagerEpoch?.manager.getView();
if (
!this.hass ||
!this.view ||
!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(this.view.query)
!MediaQueriesClassifier.areEventQueries(view.query)
) {
return;
}
// Convert the query to a clips equivalent.
const clipQuery = this.view.query.clone();
const clipQuery = view.query.clone();
clipQuery.convertToClipsQueries();
const queries = clipQuery.getQueries();
@@ -810,32 +778,18 @@ export class FrigateCardViewerProvider
return;
}
let mediaArray: ViewMedia[] | null;
try {
mediaArray = await this.cameraManager.executeMediaQueries(queries);
} catch (e) {
errorToConsole(e as Error);
return;
}
if (!mediaArray) {
return;
}
const results = new MediaQueriesResults({ results: mediaArray });
results.selectResultIfFound(
(clipMedia) => clipMedia.getID() === this.media?.getID(),
);
if (!results.hasSelectedResult()) {
return;
}
this.view
.evolve({
await this.viewManagerEpoch?.manager.setViewByParametersWithExistingQuery({
params: {
view: 'media',
query: clipQuery,
queryResults: results,
})
.dispatchChangeEvent(this);
},
queryExecutorOptions: {
selectResult: {
id: this.media.getID() ?? undefined,
},
rejectResults: (results) => !results.hasSelectedResult(),
},
});
}
protected willUpdate(changedProps: PropertyValues): void {
@@ -881,6 +835,7 @@ export class FrigateCardViewerProvider
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` <frigate-card-zoomer
@@ -892,13 +847,11 @@ export class FrigateCardViewerProvider
}
: undefined,
)}
.settings=${mediaID
? this.view?.context?.zoom?.[mediaID]?.requested
: 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<ZoomSettingsObserved>) =>
handleZoomSettingsObservedEvent(this, ev, mediaID)}
handleZoomSettingsObservedEvent(ev, this.viewManagerEpoch?.manager, mediaID)}
>
${template}
</frigate-card-zoomer>`
@@ -906,7 +859,7 @@ export class FrigateCardViewerProvider
}
protected render(): TemplateResult | void {
if (!this.load || !this.media || !this.hass || !this.view || !this.viewerConfig) {
if (!this.load || !this.media || !this.hass || !this.viewerConfig) {
return;
}