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
+36 -64
View File
@@ -24,20 +24,16 @@ import galleryStyle from '../scss/gallery.scss';
import { ExtendedHomeAssistant } from '../types.js';
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
import { errorToConsole, sleep } from '../utils/basic';
import {
changeViewToRecentEventsForCameraAndDependents,
changeViewToRecentRecordingForCameraAndDependents,
} from '../utils/media-to-view.js';
import { ViewMedia } from '../view/media';
import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries';
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
import { MediaQueriesResults } from '../view/media-queries-results';
import { View } from '../view/view.js';
import './media-filter';
import { renderMessage, renderProgressIndicator } from './message.js';
import './surround-basic';
import './thumbnail.js';
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
import { ViewManagerEpoch } from '../card-controller/view/types.js';
const GALLERY_MEDIA_FILTER_MENU_ICONS = {
closed: 'mdi:filter-cog-outline',
@@ -52,7 +48,7 @@ export class FrigateCardGallery extends LitElement {
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public view?: Readonly<View>;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false })
public galleryConfig?: GalleryConfig;
@@ -68,43 +64,16 @@ export class FrigateCardGallery extends LitElement {
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
const view = this.viewManagerEpoch?.manager.getView();
if (
!this.hass ||
!this.view ||
!this.view.isGalleryView() ||
!view?.isGalleryView() ||
!this.cameraManager ||
!this.cardWideConfig
) {
return;
}
if (!this.view.query) {
if (this.view.is('recordings')) {
changeViewToRecentRecordingForCameraAndDependents(
this,
this.cameraManager,
this.cardWideConfig,
this.view,
);
} else {
const eventsMediaType = this.view.is('snapshots')
? 'snapshots'
: this.view.is('clips')
? 'clips'
: null;
changeViewToRecentEventsForCameraAndDependents(
this,
this.cameraManager,
this.cardWideConfig,
this.view,
{
...(eventsMediaType && { eventsMediaType: eventsMediaType }),
},
);
}
return renderProgressIndicator({ cardWideConfig: this.cardWideConfig });
}
return html`
<frigate-card-surround-basic
.drawerIcons=${{
@@ -118,7 +87,7 @@ export class FrigateCardGallery extends LitElement {
? html` <frigate-card-media-filter
.hass=${this.hass}
.cameraManager=${this.cameraManager}
.view=${this.view}
.viewManagerEpoch=${this.viewManagerEpoch}
.cardWideConfig=${this.cardWideConfig}
slot=${this.galleryConfig.controls.filter.mode}
>
@@ -126,7 +95,7 @@ export class FrigateCardGallery extends LitElement {
: ''}
<frigate-card-gallery-core
.hass=${this.hass}
.view=${this.view}
.viewManagerEpoch=${this.viewManagerEpoch}
.galleryConfig=${this.galleryConfig}
.cameraManager=${this.cameraManager}
.cardWideConfig=${this.cardWideConfig}
@@ -147,7 +116,7 @@ export class FrigateCardGalleryCore extends LitElement {
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public view?: Readonly<View>;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false })
public galleryConfig?: GalleryConfig;
@@ -328,13 +297,15 @@ export class FrigateCardGalleryCore extends LitElement {
direction: 'earlier' | 'later',
useCache = true,
): Promise<void> {
if (!this.cameraManager || !this.hass || !this.view) {
const view = this.viewManagerEpoch?.manager.getView();
if (!this.cameraManager || !this.hass || !view) {
return;
}
const query = this.view?.query;
const query = view.query;
const rawQueries = query?.getQueries() ?? null;
const existingMedia = this.view.queryResults?.getResults();
const existingMedia = view.queryResults?.getResults();
if (!query || !rawQueries || !existingMedia) {
return;
}
@@ -362,16 +333,17 @@ export class FrigateCardGalleryCore extends LitElement {
: null;
if (newMediaQueries) {
this.view
?.evolve({
this.viewManagerEpoch?.manager.setViewByParameters({
baseView: view,
params: {
query: newMediaQueries,
queryResults: new MediaQueriesResults({
results: extension.results,
}).selectResultIfFound(
(media) => media === this.view?.queryResults?.getSelectedResult(),
(media) => media === view.queryResults?.getSelectedResult(),
),
})
.dispatchChangeEvent(this);
},
});
}
}
}
@@ -395,19 +367,18 @@ export class FrigateCardGalleryCore extends LitElement {
);
}
}
if (changedProps.has('view')) {
if (changedProps.has('viewManagerEpoch')) {
// If the view changes, always render the bottom loader to allow for the
// view to be extended once the bottom loader becomes visible.
this._showLoaderBottom = true;
const oldView: View | undefined = changedProps.get('view');
if (
oldView?.queryResults?.getResults() !== this.view?.queryResults?.getResults()
) {
const view = this.viewManagerEpoch?.manager.getView();
const oldView = this.viewManagerEpoch?.oldView;
if (!this._media || oldView?.queryResults?.getResults() !== view?.queryResults?.getResults()) {
// Gallery places the most recent media at the top (the query results place
// the most recent media at the end for use in the viewer). This is copied
// to a new array to avoid reversing the query results in place.
this._media = [...(this.view?.queryResults?.getResults() ?? [])].reverse();
this._media = [...(view?.queryResults?.getResults() ?? [])].reverse();
}
}
}
@@ -417,11 +388,12 @@ export class FrigateCardGalleryCore extends LitElement {
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
if (!this._media || !this.hass || !this.view || !this.view.isGalleryView()) {
if (!this._media || !this.hass) {
return html``;
}
if ((this.view?.queryResults?.getResultsCount() ?? 0) === 0) {
const view = this.viewManagerEpoch?.manager.getView();
if (!view?.queryResults || view.queryResults.getResultsCount() === 0) {
// Note that this is not throwing up an error message for the card to
// handle (as typical), but rather directly rendering the message into the
// gallery. This is to allow the filter to still be available when a given
@@ -433,7 +405,7 @@ export class FrigateCardGalleryCore extends LitElement {
});
}
const selected = this.view?.queryResults?.getSelectedResult();
const selected = view.queryResults.getSelectedResult();
return html` <div class="grid">
${this._showLoaderTop
? html`${renderProgressIndicator({
@@ -454,7 +426,7 @@ export class FrigateCardGalleryCore extends LitElement {
.hass=${this.hass}
.cameraManager=${this.cameraManager}
.media=${media}
.view=${this.view}
.viewManagerEpoch=${this.viewManagerEpoch}
?details=${!!this.galleryConfig?.controls.thumbnails.show_details}
?show_favorite_control=${!!this.galleryConfig?.controls.thumbnails
.show_favorite_control}
@@ -463,17 +435,17 @@ export class FrigateCardGalleryCore extends LitElement {
?show_download_control=${!!this.galleryConfig?.controls.thumbnails
.show_download_control}
@click=${(ev: Event) => {
if (this.view && this._media) {
this.view
.evolve({
if (this._media) {
this.viewManagerEpoch?.manager.setViewByParameters({
params: {
view: 'media',
queryResults: this.view.queryResults?.clone().selectIndex(
queryResults: view.queryResults?.clone().selectIndex(
// Media in the gallery is reversed vs the queryResults (see
// note above).
this._media.length - index - 1,
),
})
.dispatchChangeEvent(this);
},
});
}
stopEventFromActivatingCardWideActions(ev);
}}
@@ -504,9 +476,9 @@ export class FrigateCardGalleryCore extends LitElement {
// See: https://github.com/dermotduffy/frigate-hass-card/issues/885
if (
// If this update cycle updated the view ...
changedProps.has('view') &&
changedProps.has('viewManagerEpoch') &&
// ... and it wasn't set at all prior ...
!changedProps.get('view') &&
!changedProps.get('viewManagerEpoch') &&
// ... and there is a thumbnail rendered that is selected.
this._refSelected.value
) {
+63 -87
View File
@@ -19,8 +19,14 @@ import {
getOverriddenConfig,
} from '../../card-controller/conditions-manager.js';
import { ReadonlyMicrophoneManager } from '../../card-controller/microphone-manager.js';
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
import { LiveController } from '../../components-lib/live/live-controller.js';
import { MediaGridSelected } from '../../components-lib/media-grid-controller.js';
import {
PartialZoomSettings,
ZoomSettingsObserved,
} from '../../components-lib/zoom/types.js';
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
import {
CameraConfig,
CardWideConfig,
@@ -48,7 +54,8 @@ import { getStateObjOrDispatchError } from '../../utils/get-state-obj.js';
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
import { playMediaMutingIfNecessary } from '../../utils/media.js';
import { dispatchViewContextChangeEvent, View } from '../../view/view.js';
import { getStreamCameraID } from '../../utils/substream.js';
import { View } from '../../view/view.js';
import { EmblaCarouselPlugins } from '../carousel.js';
import { renderMessage } from '../message.js';
import '../next-prev-control.js';
@@ -60,12 +67,6 @@ import {
FrigateCardTitleControl,
getDefaultTitleConfigForView,
} from '../title-control.js';
import {
PartialZoomSettings,
ZoomSettingsObserved,
} from '../../components-lib/zoom/types.js';
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
import { getStreamCameraID } from '../../utils/substream.js';
const FRIGATE_CARD_LIVE_PROVIDER = 'frigate-card-live-provider';
@@ -78,7 +79,7 @@ export class FrigateCardLive extends LitElement {
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public view?: Readonly<View>;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false })
public nonOverriddenLiveConfig?: LiveConfig;
@@ -108,38 +109,16 @@ export class FrigateCardLive extends LitElement {
return this._controller.shouldUpdate();
}
protected willUpdate(changedProperties: PropertyValues): void {
if (
['view', 'cameraManager', 'cardWideConfig', 'overriddenLiveConfig'].some((prop) =>
changedProperties.has(prop),
) &&
this.view &&
this.cameraManager &&
this.cardWideConfig &&
this.overriddenLiveConfig
) {
this._controller.fetchMediaInBackgroundIfNecessary(
this.view,
this.cameraManager,
this.cardWideConfig,
this.overriddenLiveConfig,
);
}
protected willUpdate(): void {
this._controller.clearMessageReceived();
}
protected render(): TemplateResult | void {
if (
!this.hass ||
!this.nonOverriddenLiveConfig ||
!this.cameraManager ||
!this.view
) {
if (!this.hass || !this.nonOverriddenLiveConfig || !this.cameraManager) {
return;
}
// Notes:
// 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).
@@ -153,7 +132,7 @@ export class FrigateCardLive extends LitElement {
html`
<frigate-card-live-grid
.hass=${this.hass}
.view=${this.view}
.viewManagerEpoch=${this.viewManagerEpoch}
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
.overriddenLiveConfig=${this.overriddenLiveConfig}
.inBackground=${this._controller.isInBackground()}
@@ -180,7 +159,7 @@ export class FrigateCardLiveGrid extends LitElement {
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public view?: Readonly<View>;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false })
public nonOverriddenLiveConfig?: LiveConfig;
@@ -207,13 +186,14 @@ export class FrigateCardLiveGrid extends LitElement {
public triggeredCameraIDs?: Set<string>;
protected _renderCarousel(cameraID?: string): TemplateResult {
const triggeredCameraID = cameraID ?? this.view?.camera;
const view = this.viewManagerEpoch?.manager.getView();
const triggeredCameraID = cameraID ?? view?.camera;
return html`
<frigate-card-live-carousel
grid-id=${ifDefined(cameraID)}
.hass=${this.hass}
.view=${this.view}
.viewManagerEpoch=${this.viewManagerEpoch}
.viewFilterCameraID=${cameraID}
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
.overriddenLiveConfig=${this.overriddenLiveConfig}
@@ -229,26 +209,27 @@ export class FrigateCardLiveGrid extends LitElement {
`;
}
protected _gridSelectCamera(cameraID: string, view?: View): void {
(view ?? this.view)
?.evolve({
protected _gridSelectCamera(cameraID: string): void {
this.viewManagerEpoch?.manager.setViewByParameters({
params: {
camera: cameraID,
})
.dispatchChangeEvent(this);
},
});
}
protected _needsGrid(): boolean {
const cameraIDs = this.cameraManager?.getStore().getCameraIDsWithCapability('live');
const view = this.viewManagerEpoch?.manager.getView();
return (
!!this.view?.isGrid() &&
!!this.view?.supportsMultipleDisplayModes() &&
!!view?.isGrid() &&
!!view?.supportsMultipleDisplayModes() &&
!!cameraIDs &&
cameraIDs.size > 1
);
}
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('view') && this._needsGrid()) {
if (changedProps.has('viewManagerEpoch') && this._needsGrid()) {
import('../media-grid.js');
}
}
@@ -261,16 +242,13 @@ export class FrigateCardLiveGrid extends LitElement {
if (!cameraIDs?.size || !this._needsGrid()) {
return this._renderCarousel();
}
return html`
<frigate-card-media-grid
.selected=${this.view?.camera}
.selected=${this.viewManagerEpoch?.manager.getView()?.camera}
.displayConfig=${this.overriddenLiveConfig?.display}
@frigate-card:media-grid:selected=${(ev: CustomEvent<MediaGridSelected>) =>
this._gridSelectCamera(ev.detail.selected)}
@frigate-card:view:change=${(ev: CustomEvent<View>) => {
ev.stopPropagation();
this._gridSelectCamera(ev.detail.camera, ev.detail);
}}
>
${[...cameraIDs].map((cameraID) => this._renderCarousel(cameraID))}
</frigate-card-media-grid>
@@ -288,7 +266,7 @@ export class FrigateCardLiveCarousel extends LitElement {
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public view?: Readonly<View>;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false })
public nonOverriddenLiveConfig?: LiveConfig;
@@ -331,12 +309,13 @@ export class FrigateCardLiveCarousel extends LitElement {
protected _getSelectedCameraIndex(): number {
const cameraIDs = this.cameraManager?.getStore().getCameraIDsWithCapability('live');
if (!cameraIDs?.size || !this.view || this.viewFilterCameraID) {
const view = this.viewManagerEpoch?.manager.getView();
if (!cameraIDs?.size || !view || this.viewFilterCameraID) {
// If the carousel is limited to a single cameraID, the first (only)
// element is always the selected one.
return 0;
}
return Math.max(0, Array.from(cameraIDs).indexOf(this.view.camera));
return Math.max(0, Array.from(cameraIDs).indexOf(view.camera));
}
protected _getPlugins(): EmblaCarouselPlugins {
@@ -393,6 +372,7 @@ export class FrigateCardLiveCarousel extends LitElement {
return [[], {}];
}
const view = this.viewManagerEpoch?.manager.getView();
const cameraIDs = this.viewFilterCameraID
? new Set([this.viewFilterCameraID])
: this.cameraManager?.getStore().getCameraIDsWithCapability('live');
@@ -403,8 +383,7 @@ export class FrigateCardLiveCarousel extends LitElement {
for (const [cameraID, cameraConfig] of this.cameraManager
.getStore()
.getCameraConfigEntries(cameraIDs)) {
const liveCameraID =
this.view?.context?.live?.overrides?.get(cameraID) ?? cameraID;
const liveCameraID = this._getSubstreamCameraID(cameraID, view);
const liveCameraConfig =
cameraID === liveCameraID
? cameraConfig
@@ -430,17 +409,11 @@ export class FrigateCardLiveCarousel extends LitElement {
protected _setViewCameraID(cameraID?: string | null): void {
if (cameraID) {
this.view
?.evolve({
this.viewManagerEpoch?.manager.setViewByParametersWithNewQuery({
params: {
camera: cameraID,
// Reset the query and query results.
query: null,
queryResults: null,
})
// Don't yet fetch thumbnails (they will be fetched when the carousel
// settles).
.mergeInContext({ live: { fetchThumbnails: false } })
.dispatchChangeEvent(this);
},
});
}
}
@@ -490,12 +463,13 @@ export class FrigateCardLiveCarousel extends LitElement {
).live as LiveConfig;
const cameraMetadata = this.cameraManager.getCameraMetadata(cameraID);
const view = this.viewManagerEpoch?.manager.getView();
return html`
<div class="embla__slide">
<frigate-card-live-provider
?load=${!liveConfig.lazy_load}
.microphoneStream=${this.view?.camera === cameraID
.microphoneStream=${view?.camera === cameraID
? this.microphoneManager?.getStream()
: undefined}
.cameraConfig=${cameraConfig}
@@ -507,9 +481,13 @@ export class FrigateCardLiveCarousel extends LitElement {
.liveConfig=${liveConfig}
.hass=${this.hass}
.cardWideConfig=${this.cardWideConfig}
.zoomSettings=${this.view?.context?.zoom?.[cameraID]?.requested}
.zoomSettings=${view?.context?.zoom?.[cameraID]?.requested}
@frigate-card:zoom:change=${(ev: CustomEvent<ZoomSettingsObserved>) =>
handleZoomSettingsObservedEvent(this, ev, cameraID)}
handleZoomSettingsObservedEvent(
ev,
this.viewManagerEpoch?.manager,
cameraID,
)}
>
</frigate-card-live-provider>
</div>
@@ -520,11 +498,13 @@ export class FrigateCardLiveCarousel extends LitElement {
const cameraIDs = this.cameraManager
? [...this.cameraManager?.getStore().getCameraIDsWithCapability('live')]
: [];
if (this.viewFilterCameraID || cameraIDs.length <= 1 || !this.view || !this.hass) {
const view = this.viewManagerEpoch?.manager.getView();
if (this.viewFilterCameraID || cameraIDs.length <= 1 || !view || !this.hass) {
return [null, null];
}
const cameraID = this.viewFilterCameraID ?? this.view.camera;
const cameraID = this.viewFilterCameraID ?? view.camera;
const currentIndex = cameraIDs.indexOf(cameraID);
if (currentIndex < 0) {
@@ -537,8 +517,13 @@ export class FrigateCardLiveCarousel extends LitElement {
];
}
protected _getSubstreamCameraID(cameraID: string, view?: View | null): string {
return view?.context?.live?.overrides?.get(cameraID) ?? cameraID;
}
protected render(): TemplateResult | void {
if (!this.overriddenLiveConfig || !this.view || !this.hass || !this.cameraManager) {
const view = this.viewManagerEpoch?.manager.getView();
if (!this.overriddenLiveConfig || !this.hass || !view || !this.cameraManager) {
return;
}
@@ -551,23 +536,19 @@ export class FrigateCardLiveCarousel extends LitElement {
const hasMultipleCameras = slides.length > 1;
const [prevID, nextID] = this._getCameraIDsOfNeighbors();
const getOverrideCameraID = (cameraID: string): string => {
return this.view?.context?.live?.overrides?.get(cameraID) ?? cameraID;
};
const cameraMetadataPrevious = prevID
? this.cameraManager.getCameraMetadata(getOverrideCameraID(prevID))
? this.cameraManager.getCameraMetadata(this._getSubstreamCameraID(prevID, view))
: null;
const cameraID = this.viewFilterCameraID ?? this.view.camera;
const cameraID = this.viewFilterCameraID ?? view.camera;
const cameraMetadataCurrent = this.cameraManager.getCameraMetadata(
getOverrideCameraID(cameraID),
this._getSubstreamCameraID(cameraID, view),
);
const cameraMetadataNext = nextID
? this.cameraManager.getCameraMetadata(getOverrideCameraID(nextID))
? this.cameraManager.getCameraMetadata(this._getSubstreamCameraID(nextID, view))
: null;
const titleConfig = getDefaultTitleConfigForView(
this.view,
view,
this.overriddenLiveConfig?.controls.title,
);
@@ -592,10 +573,6 @@ export class FrigateCardLiveCarousel extends LitElement {
.selected=${this._getSelectedCameraIndex()}
transitionEffect=${this._getTransitionEffect()}
@frigate-card:carousel:select=${this._setViewHandler.bind(this)}
@frigate-card:carousel:settle=${() => {
// Fetch the thumbnails after the carousel has settled.
dispatchViewContextChangeEvent(this, { live: { fetchThumbnails: true } });
}}
@frigate-card:media:loaded=${() => {
if (this._refTitleControl.value) {
this._refTitleControl.value.show();
@@ -639,9 +616,8 @@ export class FrigateCardLiveCarousel extends LitElement {
<frigate-card-ptz
.config=${this.overriddenLiveConfig.controls.ptz}
.cameraManager=${this.cameraManager}
.cameraID=${getStreamCameraID(this.view, cameraID)}
.forceVisibility=${this._mediaHasLoaded &&
this.view.context?.ptzControls?.enabled}
.cameraID=${getStreamCameraID(view, cameraID)}
.forceVisibility=${this._mediaHasLoaded && view.context?.ptzControls?.enabled}
>
</frigate-card-ptz>
${cameraMetadataCurrent && titleConfig
+23 -23
View File
@@ -21,11 +21,11 @@ import {
import { CardWideConfig } from '../config/types';
import { localize } from '../localize/localize';
import mediaFilterStyle from '../scss/media-filter.scss';
import { View } from '../view/view';
import { FrigateCardDatePicker } from './date-picker';
import './date-picker.js';
import { FrigateCardSelect } from './select';
import './select.js';
import { ViewManagerEpoch } from '../card-controller/view/types';
@customElement('frigate-card-media-filter')
class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
@@ -36,7 +36,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
public cameraManager?: CameraManager;
@property({ attribute: false })
public view?: View;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false })
public cardWideConfig?: CardWideConfig;
@@ -59,46 +59,49 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
protected _refTags: Ref<FrigateCardSelect> = createRef();
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('viewManagerEpoch')) {
this._mediaFilterController.setViewManager(this.viewManagerEpoch?.manager ?? null);
}
if (changedProps.has('cameraManager') && this.cameraManager) {
this._mediaFilterController.computeCameraOptions(this.cameraManager);
this._mediaFilterController.computeMetadataOptions(this.cameraManager);
}
// The first time the viewManager is set, compute the initial default selections.
if (
changedProps.has('view') &&
!changedProps.get('view') &&
this.view &&
!changedProps.get('viewManager') &&
this.viewManagerEpoch &&
this.cameraManager
) {
this._mediaFilterController.computeInitialDefaultsFromView(
this.cameraManager,
this.view,
);
this._mediaFilterController.computeInitialDefaultsFromView(this.cameraManager);
}
}
protected render(): TemplateResult | void {
const valueChange = async () => {
if (!this.cameraManager || !this.view || !this.cardWideConfig) {
if (!this.cameraManager || !this.viewManagerEpoch || !this.cardWideConfig) {
return;
}
await this._mediaFilterController.valueChangeHandler(
this.cameraManager,
this.view,
this.cardWideConfig,
{
camera: this._refCamera.value?.value,
mediaType: this._refMediaType.value?.value as MediaFilterMediaType | undefined,
camera: this._refCamera.value?.value ?? undefined,
mediaType: (this._refMediaType.value?.value ?? undefined) as
| MediaFilterMediaType
| undefined,
when: {
selected: this._refWhen.value?.value,
selected: this._refWhen.value?.value ?? undefined,
from: this._refWhenFrom.value?.value,
to: this._refWhenTo.value?.value,
},
favorite: this._refFavorite.value?.value as
favorite: (this._refFavorite.value?.value ?? undefined) as
| MediaFilterCoreFavoriteSelection
| undefined,
where: this._refWhere.value?.value,
what: this._refWhat.value?.value,
tags: this._refTags.value?.value,
where: this._refWhere.value?.value ?? undefined,
what: this._refWhat.value?.value ?? undefined,
tags: this._refTags.value?.value ?? undefined,
},
);
};
@@ -119,14 +122,11 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
await valueChange();
};
if (!this.cameraManager || !this.view) {
if (!this.cameraManager || !this.viewManagerEpoch) {
return;
}
const controls = this._mediaFilterController.getControlsToShow(
this.cameraManager,
this.view,
);
const controls = this._mediaFilterController.getControlsToShow(this.cameraManager);
const defaults = this._mediaFilterController.getDefaults();
const whatOptions = this._mediaFilterController.getWhatOptions();
const tagsOptions = this._mediaFilterController.getTagsOptions();
+10 -3
View File
@@ -31,7 +31,7 @@ export class FrigateCardSelect extends ScopedRegistryHost(LitElement) {
public options?: SelectOption[];
@property({ attribute: false, hasChanged: contentsChanged })
public value?: SelectValues;
public value: SelectValues | null = null;
@property({ attribute: false, hasChanged: contentsChanged })
public initialValue?: SelectValues;
@@ -56,7 +56,7 @@ export class FrigateCardSelect extends ScopedRegistryHost(LitElement) {
};
public reset(): void {
this.value = undefined;
this.value = null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -66,8 +66,15 @@ export class FrigateCardSelect extends ScopedRegistryHost(LitElement) {
// the change event even if the value has not actually changed. Prevent that
// from propagating upwards.
if (value !== undefined && !isEqual(this.value, value)) {
const initialValueSet = this.value === null;
this.value = value;
dispatchFrigateCardEvent(this, 'select:change', value);
// The underlying gr-select element will call on the first first value set
// (even when the user has not interacted with the control). Do not
// dispatch events for this.
if (!initialValueSet) {
dispatchFrigateCardEvent(this, 'select:change', value);
}
}
}
+32 -28
View File
@@ -8,6 +8,8 @@ import {
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { CameraManager } from '../camera-manager/manager.js';
import { RemoveContextViewModifier } from '../card-controller/view/modifiers/remove-context.js';
import { ViewManagerEpoch } from '../card-controller/view/types.js';
import {
CardWideConfig,
MiniTimelineControlConfig,
@@ -16,7 +18,6 @@ import {
import basicBlockStyle from '../scss/basic-block.scss';
import { ExtendedHomeAssistant } from '../types.js';
import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js';
import { View } from '../view/view.js';
import './surround-basic.js';
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
@@ -26,7 +27,7 @@ export class FrigateCardSurround extends LitElement {
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public view?: Readonly<View>;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false, hasChanged: contentsChanged })
public thumbnailConfig?: ThumbnailsControlConfig;
@@ -60,42 +61,47 @@ export class FrigateCardSurround extends LitElement {
// Only reset the timeline cameraIDs when the media or display mode
// materially changes (and not on every view change, since the view will
// change frequently when the user is scrubbing video).
const oldView = changedProperties.get('view');
const view = this.viewManagerEpoch?.manager.getView();
if (
changedProperties.has('view') &&
(View.isMajorMediaChange(oldView, this.view) ||
oldView.displayMode !== this.view?.displayMode)
changedProperties.has('viewManagerEpoch') &&
(this.viewManagerEpoch?.manager.hasMajorMediaChange(
this.viewManagerEpoch?.oldView,
) ||
this.viewManagerEpoch?.oldView?.displayMode !== view?.displayMode)
) {
this._cameraIDsForTimeline = this._getCameraIDsForTimeline() ?? undefined;
}
}
protected _getCameraIDsForTimeline(): Set<string> | null {
if (!this.view || !this.cameraManager) {
const view = this.viewManagerEpoch?.manager.getView();
if (!view || !this.cameraManager) {
return null;
}
if (this.view.is('live')) {
if (view.is('live')) {
const capabilitySearch = {
anyCapabilities: ['clips' as const, 'snapshots' as const, 'recordings' as const],
};
if (this.view.supportsMultipleDisplayModes() && this.view.isGrid()) {
if (view.supportsMultipleDisplayModes() && view.isGrid()) {
return this.cameraManager
.getStore()
.getCameraIDsWithCapability(capabilitySearch);
} else {
return this.cameraManager
.getStore()
.getAllDependentCameras(this.view.camera, capabilitySearch);
.getAllDependentCameras(view.camera, capabilitySearch);
}
}
if (this.view.isViewerView()) {
return this.view.query?.getQueryCameraIDs() ?? null;
if (view.isViewerView()) {
return view.query?.getQueryCameraIDs() ?? null;
}
return null;
}
protected render(): TemplateResult | void {
if (!this.hass || !this.view) {
const view = this.viewManagerEpoch?.manager.getView();
if (!this.hass || !view) {
return;
}
@@ -122,27 +128,25 @@ export class FrigateCardSurround extends LitElement {
.hass=${this.hass}
.config=${this.thumbnailConfig}
.cameraManager=${this.cameraManager}
.fadeThumbnails=${this.view.isViewerView()}
.view=${this.view}
.selected=${this.view.queryResults?.getSelectedIndex() ?? undefined}
@frigate-card:view:change=${(ev: CustomEvent) => changeDrawer(ev, 'close')}
.fadeThumbnails=${view.isViewerView()}
.viewManagerEpoch=${this.viewManagerEpoch}
.selected=${view.queryResults?.getSelectedIndex() ?? undefined}
@frigate-card:thumbnail-carousel:tap=${(
ev: CustomEvent<ThumbnailCarouselTap>,
) => {
const media = ev.detail.queryResults.getSelectedResult();
if (media) {
this.view
?.evolve({
this.viewManagerEpoch?.manager.setViewByParameters({
params: {
view: 'media',
queryResults: ev.detail.queryResults,
...(media.getCameraID() && { camera: media.getCameraID() }),
})
.removeContext('timeline')
.removeContext('mediaViewer')
// Send the view change from the source of the tap event, so
// the view change will be caught by the handler above (to
// close the drawer).
.dispatchChangeEvent(ev.composedPath()[0]);
},
modifiers: [
new RemoveContextViewModifier(['timeline', 'mediaViewer']),
],
});
changeDrawer(ev, 'close');
}
}}
>
@@ -152,8 +156,8 @@ export class FrigateCardSurround extends LitElement {
? html` <frigate-card-timeline-core
slot=${this.timelineConfig.mode}
.hass=${this.hass}
.view=${this.view}
.itemClickAction=${this.view.isViewerView() ||
.viewManagerEpoch=${this.viewManagerEpoch}
.itemClickAction=${view.isViewerView() ||
!this.thumbnailConfig ||
this.thumbnailConfig?.mode === 'none'
? 'play'
+15 -11
View File
@@ -17,9 +17,9 @@ import { dispatchFrigateCardEvent } from '../utils/basic.js';
import { CarouselDirection } from '../utils/embla/carousel-controller.js';
import AutoSize from '../utils/embla/plugins/auto-size/auto-size.js';
import { MediaQueriesResults } from '../view/media-queries-results';
import { View } from '../view/view.js';
import './carousel.js';
import './thumbnail.js';
import { ViewManagerEpoch } from '../card-controller/view/types.js';
export interface ThumbnailCarouselTap {
queryResults: MediaQueriesResults;
@@ -31,7 +31,7 @@ export class FrigateCardThumbnailCarousel extends LitElement {
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public view?: Readonly<View>;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false })
public cameraManager?: CameraManager;
@@ -62,13 +62,13 @@ export class FrigateCardThumbnailCarousel extends LitElement {
'cameraManager',
'config',
'transitionEffect',
'view',
'viewManagerEpoch',
] as const;
if (renderProperties.some((prop) => changedProps.has(prop))) {
this._thumbnailSlides = this._renderSlides();
}
if (changedProps.has('view')) {
if (changedProps.has('viewManagerEpoch')) {
this.style.setProperty(
'--frigate-card-carousel-thumbnail-opacity',
!this.fadeThumbnails || this._getSelectedSlide() === null ? '1.0' : '0.4',
@@ -76,16 +76,19 @@ export class FrigateCardThumbnailCarousel extends LitElement {
}
}
protected _getSelectedSlide(view?: View): number | null {
return (view ?? this.view)?.queryResults?.getSelectedIndex() ?? null;
protected _getSelectedSlide(): number | null {
return (
this.viewManagerEpoch?.manager.getView()?.queryResults?.getSelectedIndex() ?? null
);
}
protected _renderSlides(): TemplateResult[] {
const slides: TemplateResult[] = [];
const seekTarget = this.view?.context?.mediaViewer?.seek;
const view = this.viewManagerEpoch?.manager.getView();
const seekTarget = view?.context?.mediaViewer?.seek;
const selectedIndex = this._getSelectedSlide();
for (const media of this.view?.queryResults?.getResults() ?? []) {
for (const media of view?.queryResults?.getResults() ?? []) {
const index = slides.length;
const classes = {
embla__slide: true,
@@ -98,19 +101,20 @@ export class FrigateCardThumbnailCarousel extends LitElement {
.cameraManager=${this.cameraManager}
.hass=${this.hass}
.media=${media}
.view=${this.view}
.viewManagerEpoch=${this.viewManagerEpoch}
.seek=${seekTarget && media.includesTime(seekTarget) ? seekTarget : undefined}
?details=${!!this.config?.show_details}
?show_favorite_control=${this.config?.show_favorite_control}
?show_timeline_control=${this.config?.show_timeline_control}
?show_download_control=${this.config?.show_download_control}
@click=${(ev: Event) => {
if (this.view && this.view.queryResults) {
const view = this.viewManagerEpoch?.manager.getView();
if (view && view.queryResults) {
dispatchFrigateCardEvent<ThumbnailCarouselTap>(
this,
'thumbnail-carousel:tap',
{
queryResults: this.view.queryResults.clone().selectIndex(index),
queryResults: view.queryResults.clone().selectIndex(index),
},
);
}
+23 -22
View File
@@ -1,3 +1,4 @@
import { Task, TaskStatus } from '@lit-labs/task';
import { format } from 'date-fns';
import {
CSSResult,
@@ -9,11 +10,14 @@ import {
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { CameraManager } from '../camera-manager/manager.js';
import { ViewManagerEpoch } from '../card-controller/view/types.js';
import { localize } from '../localize/localize.js';
import thumbnailDetailsStyle from '../scss/thumbnail-details.scss';
import thumbnailFeatureEventStyle from '../scss/thumbnail-feature-event.scss';
import thumbnailFeatureRecordingStyle from '../scss/thumbnail-feature-recording.scss';
import thumbnailStyle from '../scss/thumbnail.scss';
import type { ExtendedHomeAssistant } from '../types.js';
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
import {
errorToConsole,
@@ -21,17 +25,13 @@ import {
getDurationString,
prettifyTitle,
} from '../utils/basic.js';
import { downloadMedia } from '../utils/download.js';
import { renderTask } from '../utils/task.js';
import { createFetchThumbnailTask, FetchThumbnailTaskArgs } from '../utils/thumbnail.js';
import { View } from '../view/view.js';
import { Task, TaskStatus } from '@lit-labs/task';
import type { ExtendedHomeAssistant } from '../types.js';
import { EventViewMedia, RecordingViewMedia, ViewMedia } from '../view/media.js';
import { CameraManager } from '../camera-manager/manager.js';
import { ViewMediaClassifier } from '../view/media-classifier.js';
import { downloadMedia } from '../utils/download.js';
import { EventViewMedia, RecordingViewMedia, ViewMedia } from '../view/media.js';
import { dispatchFrigateCardErrorEvent } from './message.js';
import { RemoveContextViewModifier } from '../card-controller/view/modifiers/remove-context.js';
// The minimum width of a thumbnail with details enabled.
export const THUMBNAIL_DETAILS_WIDTH_MIN = 300;
@@ -335,22 +335,22 @@ export class FrigateCardThumbnailDetailsRecording extends LitElement {
@customElement('frigate-card-thumbnail')
export class FrigateCardThumbnail extends LitElement {
// Performance: During timeline scrubbing, hass may be updated
// continuously. As it is not needed for the thumbnail rendering itself, it
// does not trigger a re-render. The HomeAssistant object may be required for
// thumbnail signing (after initial signing the thumbnail is stored in a data
// URL, so the signing will not expire).
// Performance: During timeline scrubbing, hass may be updated continuously.
// As it is not needed for the thumbnail rendering itself, it does not trigger
// a re-render. The HomeAssistant object may be required for thumbnail signing
// (after initial signing the thumbnail is stored in a data URL, so the
// signing will not expire).
public hass?: ExtendedHomeAssistant;
// Performance: During timeline scrubbing, the view will be updated
// continuously. As it is not needed for the thumbnail rendering itself, it
// does not trigger a re-render.
public view?: Readonly<View>;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false })
public cameraManager?: CameraManager;
@property({ attribute: true })
@property({ attribute: false })
public media?: ViewMedia;
@property({ attribute: true, type: Boolean })
@@ -467,18 +467,19 @@ export class FrigateCardThumbnail extends LitElement {
title=${localize('thumbnail.timeline')}
@click=${(ev: Event) => {
stopEventFromActivatingCardWideActions(ev);
if (!this.view || !this.media) {
if (!this.viewManagerEpoch || !this.media) {
return;
}
this.view
.evolve({
this.viewManagerEpoch.manager.setViewByParameters({
params: {
view: 'timeline',
queryResults: this.view.queryResults
?.clone()
queryResults: this.viewManagerEpoch?.manager
.getView()
?.queryResults?.clone()
.selectResultIfFound((media) => media === this.media),
})
.removeContext('timeline')
.dispatchChangeEvent(this);
},
modifiers: [new RemoveContextViewModifier(['timeline'])],
});
}}
></ha-icon>`
: ''}
+133 -138
View File
@@ -1,14 +1,14 @@
import { add, differenceInSeconds, sub } from 'date-fns';
import {
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
html,
unsafeCSS,
} from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { createRef, ref, Ref } from 'lit/directives/ref.js';
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import isEqual from 'lodash-es/isEqual';
import throttle from 'lodash-es/throttle';
import { ViewContext } from 'view';
@@ -26,6 +26,8 @@ import { CameraManager } from '../camera-manager/manager';
import { rangesOverlap } from '../camera-manager/range';
import { MediaQuery } from '../camera-manager/types';
import { convertRangeToCacheFriendlyTimes } from '../camera-manager/utils/range-to-cache-friendly';
import { MergeContextViewModifier } from '../card-controller/view/modifiers/merge-context';
import { ViewManagerEpoch } from '../card-controller/view/types';
import {
FrigateCardTimelineItem,
TimelineDataSource,
@@ -33,11 +35,11 @@ import {
import {
CameraConfig,
CardWideConfig,
frigateCardConfigDefaults,
FrigateCardView,
ThumbnailsControlBaseConfig,
TimelineCoreConfig,
TimelinePanMode,
frigateCardConfigDefaults,
} from '../config/types';
import { localize } from '../localize/localize';
import timelineCoreStyle from '../scss/timeline-core.scss';
@@ -51,10 +53,7 @@ import {
isTruthy,
setOrRemoveAttribute,
} from '../utils/basic';
import {
executeMediaQueryForViewWithErrorDispatching,
findBestMediaIndex,
} from '../utils/media-to-view';
import { findBestMediaIndex } from '../utils/find-best-media-index';
import { ViewMedia } from '../view/media';
import { ViewMediaClassifier } from '../view/media-classifier';
import {
@@ -67,7 +66,7 @@ import {
MediaQueriesType,
} from '../view/media-queries-classifier';
import { MediaQueriesResults } from '../view/media-queries-results';
import { View } from '../view/view';
import { mergeViewContext } from '../view/view';
import './date-picker.js';
import { DatePickerEvent, FrigateCardDatePicker } from './date-picker.js';
import './thumbnail.js';
@@ -107,7 +106,7 @@ interface ThumbnailDataRequest {
cameraManager?: CameraManager;
cameraConfig?: CameraConfig;
media?: ViewMedia;
view?: View;
viewManagerEpoch?: ViewManagerEpoch;
}
class ThumbnailDataRequestEvent extends CustomEvent<ThumbnailDataRequest> {}
@@ -115,7 +114,7 @@ class ThumbnailDataRequestEvent extends CustomEvent<ThumbnailDataRequest> {}
const TIMELINE_TARGET_BAR_ID = 'target_bar';
/**
* A simgple thumbnail wrapper class for use in the timeline where LIT data
* A simgple thumbnail wrapper class for use in the timeline where Lit data
* bindings are not available.
*/
@customElement('frigate-card-timeline-thumbnail')
@@ -160,7 +159,7 @@ export class FrigateCardTimelineThumbnail extends LitElement {
!dataRequest.cameraManager ||
!dataRequest.cameraConfig ||
!dataRequest.media ||
!dataRequest.view
!dataRequest.viewManagerEpoch
) {
return html``;
}
@@ -169,7 +168,7 @@ export class FrigateCardTimelineThumbnail extends LitElement {
.hass=${dataRequest.hass}
.cameraManager=${dataRequest.cameraManager}
.media=${dataRequest.media}
.view=${dataRequest.view}
.viewManagerEpoch=${dataRequest.viewManagerEpoch}
?details=${this.details}
>
</frigate-card-thumbnail>`;
@@ -182,12 +181,12 @@ export class FrigateCardTimelineCore extends LitElement {
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public view?: Readonly<View>;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false, hasChanged: contentsChanged })
public timelineConfig?: TimelineCoreConfig;
@property({ attribute: true, type: Boolean })
@property({ attribute: false })
public thumbnailConfig?: ThumbnailsControlBaseConfig;
// Whether or not this is a mini-timeline (in mini-mode the component takes a
@@ -269,11 +268,11 @@ export class FrigateCardTimelineCore extends LitElement {
request.detail.cameraConfig = cameraConfig;
request.detail.cameraManager = this.cameraManager;
request.detail.media = media;
request.detail.view = this.view;
request.detail.viewManagerEpoch = this.viewManagerEpoch;
}
protected render(): TemplateResult | void {
if (!this.hass || !this.view || !this.timelineConfig || !this.cameraIDs?.size) {
if (!this.hass || !this.timelineConfig || !this.cameraIDs?.size) {
return;
}
@@ -383,6 +382,7 @@ export class FrigateCardTimelineCore extends LitElement {
return;
}
const view = this.viewManagerEpoch?.manager.getView();
const panMode = this._getEffectivePanMode();
const targetBarOn =
this._shouldSupportSeeking() &&
@@ -392,7 +392,7 @@ export class FrigateCardTimelineCore extends LitElement {
const item = this._timelineSource?.dataset?.get(id);
return (
panMode !== 'seek-in-camera' ||
item?.media?.getCameraID() === this.view?.camera,
item?.media?.getCameraID() === view?.camera,
item &&
item.start &&
item.end &&
@@ -450,14 +450,15 @@ export class FrigateCardTimelineCore extends LitElement {
targetTime: Date,
properties: TimelineRangeChange,
): Promise<void> {
const results = this.view?.queryResults;
const view = this.viewManagerEpoch?.manager.getView();
const results = view?.queryResults;
const media = results?.getResults();
const panMode = this._getEffectivePanMode();
if (
!media ||
!results ||
!this._timeline ||
!this.view ||
!view ||
!this.hass ||
!this.cameraManager ||
panMode === 'pan'
@@ -473,7 +474,7 @@ export class FrigateCardTimelineCore extends LitElement {
.clone()
.resetSelectedResult()
.selectBestResult(
(mediaArray) => findBestMediaIndex(mediaArray, targetTime, this.view?.camera),
(mediaArray) => findBestMediaIndex(mediaArray, targetTime, view?.camera),
{
allCameras: true,
main: true,
@@ -484,9 +485,9 @@ export class FrigateCardTimelineCore extends LitElement {
.clone()
.resetSelectedResult()
.selectBestResult((mediaArray) => findBestMediaIndex(mediaArray, targetTime), {
cameraID: this.view.camera,
cameraID: view.camera,
})
.promoteCameraSelectionToMainSelection(this.view.camera);
.promoteCameraSelectionToMainSelection(view.camera);
} else if (panMode === 'seek-in-media') {
newResults = results;
}
@@ -495,20 +496,23 @@ export class FrigateCardTimelineCore extends LitElement {
? targetTime >= new Date()
? 'live'
: 'media'
: this.view.view;
: view.view;
const selectedCamera = newResults?.getSelectedResult()?.getCameraID();
this.view
.evolve({
this.viewManagerEpoch?.manager.setViewByParameters({
params: {
...(selectedCamera && { camera: selectedCamera }),
view: desiredView,
queryResults: newResults,
}) // Whether or not to set the timeline window.
.mergeInContext({
...(canSeek && { mediaViewer: { seek: targetTime } }),
...this._getTimelineContext({ start: properties.start, end: properties.end }),
})
.dispatchChangeEvent(this);
},
modifiers: [
new MergeContextViewModifier({
...(canSeek && { mediaViewer: { seek: targetTime } }),
...this._getTimelineContext({ start: properties.start, end: properties.end }),
}),
],
});
}
protected _getEffectivePanMode(): TimelinePanMode {
@@ -533,22 +537,18 @@ export class FrigateCardTimelineCore extends LitElement {
stopEventFromActivatingCardWideActions(properties.event);
}
const view = this.viewManagerEpoch?.manager.getView();
if (
this._ignoreClick ||
!this.hass ||
!this._timeline ||
!this.view ||
!this.cameraManager ||
!this.cardWideConfig ||
!this.cameraIDs ||
!this.cameraIDs.size ||
!view ||
!this.viewManagerEpoch ||
!this._timelineSource ||
!properties.what
) {
return;
}
let view: View | null = null;
let drawerAction: 'open' | 'close' = 'close';
if (
@@ -558,29 +558,35 @@ export class FrigateCardTimelineCore extends LitElement {
) {
const query = this._createMediaQueries('recording');
if (query) {
view = await executeMediaQueryForViewWithErrorDispatching(
this,
this.cameraManager,
this.view,
query,
{
targetView: 'recording',
targetTime: properties.time,
select: 'time',
await this.viewManagerEpoch?.manager.setViewByParametersWithExistingQuery({
baseView: view,
params: { view: 'recording', query: query },
queryExecutorOptions: {
selectResult: {
time: {
time: properties.time,
},
},
},
);
});
}
} else if (properties.item && properties.what === 'item') {
const cameraID = String(properties.group);
const id = String(properties.item);
const criteria = {
main: true,
...(cameraID && this.view.isGrid() && { cameraID: cameraID }),
...(cameraID && view.isGrid() && { cameraID: cameraID }),
};
const newResults = this.view.queryResults
const newResults = view.queryResults
?.clone()
.resetSelectedResult()
.selectResultIfFound((media) => media.getID() === properties.item, criteria);
const context: ViewContext = mergeViewContext(this._getTimelineContext(), {
mediaViewer: { seek: properties.time },
});
if (!newResults || !newResults.hasSelectedResult()) {
// This can happen in a few situations:
// - If this is a recording query (with recorded hours) and an event is
@@ -589,36 +595,34 @@ export class FrigateCardTimelineCore extends LitElement {
// gallery (i.e. any case where the thumbnails may not be match the
// events on the timeline, e.g. in the snapshots viewer but
// mini-timeline showing all media).
const fullEventView = await this._createViewWithMediaQueries(
this._createMediaQueries('event'),
{
selectedItem: properties.item,
targetView: 'media',
},
);
if (fullEventView?.queryResults?.hasResults()) {
view = fullEventView;
const query = this._createMediaQueries('event');
if (query) {
await this.viewManagerEpoch?.manager.setViewByParametersWithExistingQuery({
params: { view: 'media', query: query },
queryExecutorOptions: {
selectResult: {
id: id,
},
rejectResults: (results) => !results.hasResults(),
},
modifiers: [new MergeContextViewModifier(context)],
});
}
} else {
view = this.view.evolve({
queryResults: newResults,
view: this.itemClickAction === 'play' ? 'media' : this.view.view,
this.viewManagerEpoch.manager.setViewByParameters({
params: {
queryResults: newResults,
view: this.itemClickAction === 'play' ? 'media' : view.view,
},
modifiers: [new MergeContextViewModifier(context)],
});
}
if (view?.queryResults?.hasResults()) {
view.mergeInContext({ mediaViewer: { seek: properties.time } });
}
view?.mergeInContext(this._getTimelineContext());
if (this.itemClickAction === 'select' && view) {
if (this.itemClickAction === 'select') {
drawerAction = 'open';
}
}
if (view) {
view.dispatchChangeEvent(this);
}
dispatchFrigateCardEvent(this, `thumbnails:${drawerAction}`);
this._ignoreClick = false;
@@ -648,10 +652,11 @@ export class FrigateCardTimelineCore extends LitElement {
event: Event & { additionalEvent: string };
}): Promise<void> {
this._removeTargetBar();
const view = this.viewManagerEpoch?.manager.getView();
if (
!this._timeline ||
!this.view ||
!view ||
// When in mini mode, something else is in charge of the primary media
// population (e.g. the live view), in this case only act when the user
// themselves are interacting with the timeline.
@@ -662,23 +667,34 @@ export class FrigateCardTimelineCore extends LitElement {
await this._timelineSource?.refresh(this._getPrefetchWindow(properties));
const queryType = MediaQueriesClassifier.getQueriesType(this.view.query);
const queryType = MediaQueriesClassifier.getQueriesType(view.query);
if (!queryType) {
return;
}
const mediaQuery = this._createMediaQueries(queryType);
const newView = await this._createViewWithMediaQueries(mediaQuery);
// Specifically avoid dispatching new results on range change unless there
// is something to be gained by doing so. Example usecase: On initial view
// load in mini timeline mode, the first 50 events are fetched -- the
// first drag of the timeline should not dispatch new results unless
// something is actually useful (as otherwise it creates a visible
// 'flicker' for the user as the viewer reloads all the media).
const newResults = newView?.queryResults;
if (newView && newResults && !this.view.queryResults?.isSupersetOf(newResults)) {
newView?.mergeInContext(this._getTimelineContext())?.dispatchChangeEvent(this);
}
await this.viewManagerEpoch?.manager.setViewByParametersWithExistingQuery({
params: {
query: mediaQuery,
},
queryExecutorOptions: {
// Reject the new results unless there is something to be gained (i.e. they
// are not a subset of the existing results). Example usecase: On initial
// view load in mini timeline mode, the first 50 events are fetched -- the
// first drag of the timeline should not dispatch new results unless
// something is actually useful (as otherwise it creates a visible 'flicker'
// for the user as the viewer reloads all the media).
rejectResults: (results) => !!view.queryResults?.isSupersetOf(results),
selectResult: {
id:
this.viewManagerEpoch?.manager
.getView()
?.queryResults?.getSelectedResult()
?.getID() ?? undefined,
},
},
modifiers: [new MergeContextViewModifier(this._getTimelineContext())],
});
}
protected _createMediaQueries(
@@ -706,46 +722,6 @@ export class FrigateCardTimelineCore extends LitElement {
return null;
}
protected async _createViewWithMediaQueries(
query: MediaQueries | null,
options?: {
targetView?: FrigateCardView;
selectedItem?: IdType;
},
): Promise<View | null> {
if (!this.hass || !this.cameraManager || !this.view || !query) {
return null;
}
const view = await executeMediaQueryForViewWithErrorDispatching(
this,
this.cameraManager,
this.view,
query,
{
targetView: options?.targetView,
select: 'latest',
},
);
if (!view) {
return null;
}
if (options?.selectedItem) {
view.queryResults?.selectResultIfFound(
(media) => media.getID() === options.selectedItem,
);
} else {
// If not asked to select a new item, persist the currently selected item
// if possible.
const currentlySelectedResult = this.view.queryResults?.getSelectedResult();
if (currentlySelectedResult) {
view.queryResults?.selectResultIfFound(
(media) => media.getID() === currentlySelectedResult.getID(),
);
}
}
return view;
}
/**
* Build the visjs dataset to render on the timeline.
* @returns The dataset.
@@ -937,10 +913,11 @@ export class FrigateCardTimelineCore extends LitElement {
}
protected _getAllSelectedMediaIDsFromView(): IdType[] {
const view = this.viewManagerEpoch?.manager.getView();
return (
this.view?.queryResults?.getMultipleSelectedResults({
view?.queryResults?.getMultipleSelectedResults({
main: true,
...(this.view.isGrid() && { allCameras: true }),
...(view.isGrid() && { allCameras: true }),
}) ?? []
)
.filter((media) => ViewMediaClassifier.isEvent(media))
@@ -952,7 +929,8 @@ export class FrigateCardTimelineCore extends LitElement {
* Update the timeline from the view object.
*/
protected async _updateTimelineFromView(): Promise<void> {
if (!this.view || !this.timelineConfig || !this._timelineSource || !this._timeline) {
const view = this.viewManagerEpoch?.manager.getView();
if (!view || !this.timelineConfig || !this._timelineSource || !this._timeline) {
return;
}
@@ -965,7 +943,7 @@ export class FrigateCardTimelineCore extends LitElement {
// perfectly center on the media.
let desiredWindow = timelineWindow;
const media = this.view.queryResults?.getSelectedResult();
const media = view.queryResults?.getSelectedResult();
const mediaStartTime = media?.getStartTime() ?? null;
const mediaEndTime = media?.getEndTime() ?? null;
const mediaIsEvent = media ? ViewMediaClassifier.isEvent(media) : false;
@@ -976,7 +954,7 @@ export class FrigateCardTimelineCore extends LitElement {
// range effectively starts/ends at the same time.
{ start: mediaStartTime, end: mediaEndTime ?? mediaStartTime }
: null;
const context = this.view.context?.timeline;
const context = view.context?.timeline;
if (context && context.window) {
desiredWindow = context.window;
@@ -1048,7 +1026,7 @@ export class FrigateCardTimelineCore extends LitElement {
// Also don't generate thumbnails in mini-timelines (they will already have
// been generated).
const queryType = MediaQueriesClassifier.getQueriesType(this.view.query);
const queryType = MediaQueriesClassifier.getQueriesType(view.query);
if (!queryType) {
return;
}
@@ -1062,15 +1040,31 @@ export class FrigateCardTimelineCore extends LitElement {
freshMediaQuery &&
!this._alreadyHasAcceptableMediaQuery(freshMediaQuery)
) {
(await this._createViewWithMediaQueries(freshMediaQuery))
?.mergeInContext(this._getTimelineContext(desiredWindow))
.dispatchChangeEvent(this);
const currentlySelectedResult = this.viewManagerEpoch?.manager
.getView()
?.queryResults?.getSelectedResult();
await this.viewManagerEpoch?.manager.setViewByParametersWithExistingQuery({
params: {
query: freshMediaQuery,
},
queryExecutorOptions: {
selectResult: {
id: currentlySelectedResult?.getID() ?? undefined,
},
},
modifiers: [
new MergeContextViewModifier(this._getTimelineContext(desiredWindow)),
],
});
}
}
protected _alreadyHasAcceptableMediaQuery(freshMediaQuery: MediaQueries): boolean {
const currentQueries = this.view?.query?.getQueries();
const currentResultTimestamp = this.view?.queryResults?.getResultsTimestamp();
const view = this.viewManagerEpoch?.manager.getView();
const currentQueries = view?.query?.getQueries();
const currentResultTimestamp = view?.queryResults?.getResultsTimestamp();
return (
!!this.cameraManager &&
@@ -1089,10 +1083,11 @@ export class FrigateCardTimelineCore extends LitElement {
* @returns The TimelineViewContext object.
*/
protected _getTimelineContext(window?: TimelineWindow): ViewContext {
const view = this.viewManagerEpoch?.manager.getView();
const newWindow = window ?? this._timeline?.getWindow();
return {
timeline: {
...this.view?.context?.timeline,
...view?.context?.timeline,
...(newWindow && { window: newWindow }),
},
};
@@ -1225,7 +1220,7 @@ export class FrigateCardTimelineCore extends LitElement {
// `this._timeline.setwindow()` being entirely ignored. Example case:
// Clicking the timeline control on a recording thumbnail.
window.requestAnimationFrame(this._updateTimelineFromView.bind(this));
} else if (changedProperties.has('view')) {
} else if (changedProperties.has('viewManagerEpoch')) {
this._updateTimelineFromView();
}
}
+3 -3
View File
@@ -1,10 +1,10 @@
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { CameraManager } from '../camera-manager/manager';
import { ViewManagerEpoch } from '../card-controller/view/types';
import { CardWideConfig, TimelineConfig } from '../config/types';
import basicBlockStyle from '../scss/basic-block.scss';
import { ExtendedHomeAssistant } from '../types';
import { View } from '../view/view';
import './surround.js';
import './timeline-core.js';
@@ -14,7 +14,7 @@ export class FrigateCardTimeline extends LitElement {
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public view?: Readonly<View>;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false })
public timelineConfig?: TimelineConfig;
@@ -33,7 +33,7 @@ export class FrigateCardTimeline extends LitElement {
return html`
<frigate-card-timeline-core
.hass=${this.hass}
.view=${this.view}
.viewManagerEpoch=${this.viewManagerEpoch}
.timelineConfig=${this.timelineConfig}
.thumbnailConfig=${this.timelineConfig.controls.thumbnails}
.cameraManager=${this.cameraManager}
+1 -1
View File
@@ -10,7 +10,7 @@ type PaperToast = HTMLElement & {
};
export const getDefaultTitleConfigForView = (
view?: Readonly<View>,
view?: Readonly<View> | null,
baseConfig?: TitleControlConfig,
): TitleControlConfig | null => {
if (!baseConfig && view?.isGrid()) {
+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;
}
+33 -30
View File
@@ -11,6 +11,7 @@ import { classMap } from 'lit/directives/class-map.js';
import { CameraManager } from '../camera-manager/manager.js';
import { ConditionsManagerEpoch } from '../card-controller/conditions-manager.js';
import { ReadonlyMicrophoneManager } from '../card-controller/microphone-manager.js';
import { ViewManagerEpoch } from '../card-controller/view/types.js';
import {
CardWideConfig,
FrigateCardConfig,
@@ -19,8 +20,6 @@ import {
import viewsStyle from '../scss/views.scss';
import { ExtendedHomeAssistant } from '../types.js';
import { ResolvedMediaCache } from '../utils/ha/resolved-media.js';
import { View } from '../view/view.js';
import './surround.js';
// As a special case: Diagnostics is not dynamically loaded in case something goes wrong.
import './diagnostics.js';
@@ -31,7 +30,7 @@ export class FrigateCardViews extends LitElement {
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public view?: Readonly<View>;
public viewManagerEpoch?: ViewManagerEpoch;
@property({ attribute: false })
public cameraManager?: CameraManager;
@@ -64,17 +63,18 @@ export class FrigateCardViews extends LitElement {
public triggeredCameraIDs?: Set<string>;
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('view') || changedProps.has('config')) {
if (this.view?.is('live') || this._shouldLivePreload()) {
if (changedProps.has('viewManagerEpoch') || changedProps.has('config')) {
const view = this.viewManagerEpoch?.manager.getView();
if (view?.is('live') || this._shouldLivePreload()) {
import('./live/live.js');
}
if (this.view?.isGalleryView()) {
if (view?.isGalleryView()) {
import('./gallery.js');
} else if (this.view?.isViewerView()) {
} else if (view?.isViewerView()) {
import('./viewer.js');
} else if (this.view?.is('image')) {
} else if (view?.is('image')) {
import('./image.js');
} else if (this.view?.is('timeline')) {
} else if (view?.is('timeline')) {
import('./timeline.js');
}
}
@@ -108,10 +108,11 @@ export class FrigateCardViews extends LitElement {
}
protected _shouldLivePreload(): boolean {
const view = this.viewManagerEpoch?.manager.getView();
return (
// Special case: Never preload for diagnostics -- we want that to be as
// minimal as possible.
!!this.overriddenConfig?.live.preload && !this.view?.is('diagnostics')
!!this.overriddenConfig?.live.preload && !view?.is('diagnostics')
);
}
@@ -129,67 +130,69 @@ export class FrigateCardViews extends LitElement {
return html``;
}
const view = this.viewManagerEpoch?.manager.getView();
// Render but hide the live view if there's a message, or if it's preload
// mode and the view is not live.
const liveClasses = {
hidden: this._shouldLivePreload() && !this.view?.is('live'),
hidden: this._shouldLivePreload() && !view?.is('live'),
};
const overallClasses = {
hidden: !!this.hide,
};
const thumbnailConfig = this.view?.is('live')
const thumbnailConfig = view?.is('live')
? this.overriddenConfig.live.controls.thumbnails
: this.view?.isViewerView()
: view?.isViewerView()
? this.overriddenConfig.media_viewer.controls.thumbnails
: this.view?.is('timeline')
: view?.is('timeline')
? this.overriddenConfig.timeline.controls.thumbnails
: undefined;
const miniTimelineConfig = this.view?.is('live')
const miniTimelineConfig = view?.is('live')
? this.overriddenConfig.live.controls.timeline
: this.view?.isViewerView()
: view?.isViewerView()
? this.overriddenConfig.media_viewer.controls.timeline
: undefined;
const cameraConfig = this.view
? this.cameraManager?.getStore().getCameraConfig(this.view.camera) ?? null
const cameraConfig = view
? this.cameraManager?.getStore().getCameraConfig(view.camera) ?? null
: null;
return html` <frigate-card-surround
class="${classMap(overallClasses)}"
.hass=${this.hass}
.view=${this.view}
.viewManagerEpoch=${this.viewManagerEpoch}
.thumbnailConfig=${!this.hide ? thumbnailConfig : undefined}
.timelineConfig=${!this.hide ? miniTimelineConfig : undefined}
.cameraManager=${this.cameraManager}
.cardWideConfig=${this.cardWideConfig}
>
${!this.hide && this.view?.is('image') && cameraConfig
${!this.hide && view?.is('image') && cameraConfig
? html` <frigate-card-image
.imageConfig=${this.overriddenConfig.image}
.view=${this.view}
.view=${view}
.hass=${this.hass}
.cameraConfig=${cameraConfig}
.cameraManager=${this.cameraManager}
>
</frigate-card-image>`
: ``}
${!this.hide && this.view?.isGalleryView()
${!this.hide && view?.isGalleryView()
? html` <frigate-card-gallery
.hass=${this.hass}
.view=${this.view}
.viewManagerEpoch=${this.viewManagerEpoch}
.galleryConfig=${this.overriddenConfig.media_gallery}
.cameraManager=${this.cameraManager}
.cardWideConfig=${this.cardWideConfig}
>
</frigate-card-gallery>`
: ``}
${!this.hide && this.view?.isViewerView()
${!this.hide && view?.isViewerView()
? html`
<frigate-card-viewer
.hass=${this.hass}
.view=${this.view}
.viewManagerEpoch=${this.viewManagerEpoch}
.viewerConfig=${this.overriddenConfig.media_viewer}
.resolvedMediaCache=${this.resolvedMediaCache}
.cameraManager=${this.cameraManager}
@@ -198,17 +201,17 @@ export class FrigateCardViews extends LitElement {
</frigate-card-viewer>
`
: ``}
${!this.hide && this.view?.is('timeline')
${!this.hide && view?.is('timeline')
? html` <frigate-card-timeline
.hass=${this.hass}
.view=${this.view}
.viewManagerEpoch=${this.viewManagerEpoch}
.timelineConfig=${this.overriddenConfig.timeline}
.cameraManager=${this.cameraManager}
.cardWideConfig=${this.cardWideConfig}
>
</frigate-card-timeline>`
: ``}
${!this.hide && this.view?.is('diagnostics')
${!this.hide && view?.is('diagnostics')
? html` <frigate-card-diagnostics
.hass=${this.hass}
.rawConfig=${this.rawConfig}
@@ -222,11 +225,11 @@ export class FrigateCardViews extends LitElement {
// Note: <frigate-card-live> uses nonOverriddenConfig rather than the
// overriden config as it does it's own overriding as part of the camera
// carousel.
this._shouldLivePreload() || (!this.hide && this.view?.is('live'))
this._shouldLivePreload() || (!this.hide && view?.is('live'))
? html`
<frigate-card-live
.hass=${this.hass}
.view=${this.view}
.viewManagerEpoch=${this.viewManagerEpoch}
.nonOverriddenLiveConfig=${this.nonOverriddenConfig.live}
.overriddenLiveConfig=${this.overriddenConfig.live}
.conditionsManagerEpoch=${this.conditionsManagerEpoch}