Refactor views to support dynamic updates.
This commit is contained in:
+36
-64
@@ -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
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user