diff --git a/src/camera/frigate/util.ts b/src/camera/frigate/util.ts index b916ada9..08debd95 100644 --- a/src/camera/frigate/util.ts +++ b/src/camera/frigate/util.ts @@ -25,7 +25,8 @@ export const getRecordingTitle = ( cameraConfig: CameraConfig, recording: FrigateRecording, ): string => { - return `${prettifyTitle(cameraConfig.frigate.camera_name)} ${formatDateAndTime( + const cameraName = prettifyTitle(cameraConfig.frigate.camera_name ?? ''); + return `${cameraName ? `${cameraName} ` : ''}${formatDateAndTime( recording.startTime, )}`; }; diff --git a/src/card.ts b/src/card.ts index 478fd580..023dd199 100644 --- a/src/card.ts +++ b/src/card.ts @@ -94,7 +94,6 @@ import { ViewContext } from 'view'; import { CameraManager } from './camera/manager.js'; import { setLowPerformanceProfile, setPerformanceCSSStyles } from './performance.js'; import { CameraManagerEngineFactory } from './camera/engine-factory.js'; -import { RequestCache } from './camera/cache.js'; /** A note on media callbacks: * diff --git a/src/components/media-filter-core.ts b/src/components/media-filter-core.ts index deb1b895..6d1c8bf1 100644 --- a/src/components/media-filter-core.ts +++ b/src/components/media-filter-core.ts @@ -232,6 +232,7 @@ export class FrigateCardMediaFilterCore extends LitElement { .label=${localize('media_filter.media_type')} .items=${this._mediaTypeOptions} .allowCustomValue=${false} + .value=${this.defaults?.mediaType} @value-changed=${this._valueChangedHandler.bind(this)} >` : ''} @@ -242,6 +243,7 @@ export class FrigateCardMediaFilterCore extends LitElement { .label=${localize('media_filter.when')} .items=${this._whenOptions} .allowCustomValue=${false} + .value=${this.defaults?.when} @value-changed=${this._valueChangedHandler.bind(this)} >` : ''} @@ -252,6 +254,9 @@ export class FrigateCardMediaFilterCore extends LitElement { .label=${localize('media_filter.camera')} .items=${this._cameraOptions} .allowCustomValue=${false} + .value=${this.defaults?.cameraIDs?.size === 1 + ? [...this.defaults.cameraIDs][0] + : undefined} @value-changed=${this._valueChangedHandler.bind(this)} >` : ''} @@ -262,6 +267,9 @@ export class FrigateCardMediaFilterCore extends LitElement { .label=${localize('media_filter.what')} .items="${this._whatOptions}" .allowCustomValue=${false} + .value=${this.defaults?.what?.size === 1 + ? [...this.defaults.what][0] + : undefined} @value-changed=${this._valueChangedHandler.bind(this)} >` : ''} @@ -272,6 +280,9 @@ export class FrigateCardMediaFilterCore extends LitElement { .label=${localize('media_filter.where')} .items=${this._whereOptions} .allowCustomValue=${false} + .value=${this.defaults?.where?.size === 1 + ? [...this.defaults.where][0] + : undefined} @value-changed=${this._valueChangedHandler.bind(this)} >` : ''} @@ -283,6 +294,7 @@ export class FrigateCardMediaFilterCore extends LitElement { .label=${localize('media_filter.favorite')} .items=${this._favoriteOptions} .allowCustomValue=${false} + .value=${this.defaults?.favorite} @value-changed=${this._valueChangedHandler.bind(this)} > ` diff --git a/src/components/media-filter.ts b/src/components/media-filter.ts index 36266909..d1c5c3a9 100644 --- a/src/components/media-filter.ts +++ b/src/components/media-filter.ts @@ -16,9 +16,7 @@ import { unsafeCSS, } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import { - CameraManager, -} from '../camera/manager'; +import { CameraManager } from '../camera/manager'; import { DateRange } from '../camera/range'; import { CameraConfig, ExtendedHomeAssistant } from '../types'; import { getCameraTitle } from '../utils/camera'; @@ -44,6 +42,8 @@ import parse from 'date-fns/parse'; import endOfMonth from 'date-fns/endOfMonth'; import mediaFilterStyle from '../scss/media-filter.scss'; import { MediaQueriesClassifier } from '../view/media-queries-classifier'; +import uniqWith from 'lodash-es/uniqWith'; +import isEqual from 'lodash-es/isEqual'; @customElement('frigate-card-media-filter') export class FrigateCardMediaFilter extends LitElement { @@ -114,21 +114,33 @@ export class FrigateCardMediaFilter extends LitElement { const convertedTime = this._convertWhenToDateRange(mediaFilter.when); const convertedFavorite = this._convertFavoriteToBoolean(mediaFilter.favorite); + const cameraIDs = mediaFilter.cameraIDs ?? new Set(this.cameras.keys()); + // A note on views: + // - In the below, if the user selects a camera to view media for, the main + // view camera is also set to that value (e.g. a user browsing the + // gallery, chooses a different camera in the media filter, then + // subsequently chooses the live button -- they would expect the live view + // for that filtered camera not the prior camera). + // - Similarly, if the user chooses clips or snapshots, set the actual view + // to 'clips' or 'snapshots' in order to ensure the right icon is shown as + // selected in the menu. if ( mediaFilter.mediaType === MediaFilterMediaType.Clips || mediaFilter.mediaType === MediaFilterMediaType.Snapshots ) { const query: EventQuery = { type: QueryType.Event, - cameraIDs: mediaFilter.cameraIDs ?? new Set(this.cameras.keys()), + cameraIDs: cameraIDs, ...(mediaFilter.what && { what: mediaFilter.what }), ...(mediaFilter.where && { where: mediaFilter.where }), ...(convertedFavorite !== null && { favorite: convertedFavorite }), ...(convertedTime && { start: convertedTime.start, end: convertedTime.end }), ...(this.mediaLimit && { limit: this.mediaLimit }), ...(mediaFilter.mediaType === MediaFilterMediaType.Clips && { hasClip: true }), - ...(mediaFilter.mediaType === MediaFilterMediaType.Snapshots && { hasSnapshot: true }) + ...(mediaFilter.mediaType === MediaFilterMediaType.Snapshots && { + hasSnapshot: true, + }), }; ( @@ -140,13 +152,20 @@ export class FrigateCardMediaFilter extends LitElement { this.view, { query: new EventMediaQueries([query]), + + // See 'A note on views' above for these two arguments. + ...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }), + targetView: + mediaFilter.mediaType === MediaFilterMediaType.Clips + ? 'clips' + : 'snapshots', }, ) )?.dispatchChangeEvent(this); } else if (mediaFilter.mediaType === MediaFilterMediaType.Recordings) { const query: RecordingQuery = { type: QueryType.Recording, - cameraIDs: mediaFilter.cameraIDs ?? new Set(this.cameras.keys()), + cameraIDs: cameraIDs, ...(convertedTime && { start: convertedTime.start, end: convertedTime.end }), }; @@ -159,6 +178,10 @@ export class FrigateCardMediaFilter extends LitElement { this.view, { query: new RecordingMediaQueries([query]), + + // See 'A note on views' above for these two arguments. + ...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }), + targetView: 'recordings', }, ) )?.dispatchChangeEvent(this); @@ -184,6 +207,82 @@ export class FrigateCardMediaFilter extends LitElement { } } + protected _getDefaultsFromView(): MediaFilterCoreSelection | undefined { + if (!this.view) { + return undefined; + } + + let mediaType: MediaFilterMediaType | undefined; + let cameraIDs: Set | undefined; + let what: Set | undefined; + let where: Set | undefined; + let favorite: boolean | undefined; + + if (MediaQueriesClassifier.areEventQueries(this.view.query)) { + const queries = this.view.query.getQueries(); + if (!queries) { + return; + } + + const hasClips = uniqWith( + queries.map((query) => query.hasClip), + isEqual, + ); + const hasSnapshots = uniqWith( + queries.map((query) => query.hasSnapshot), + isEqual, + ); + if (hasClips.length === 1 && hasSnapshots.length === 1) { + mediaType = !!hasClips[0] + ? MediaFilterMediaType.Clips + : !!hasSnapshots[0] + ? MediaFilterMediaType.Snapshots + : undefined; + } + + const cameraIDSets = uniqWith( + queries.map((query) => query.cameraIDs), + isEqual, + ); + if (cameraIDSets.length === 1) { + cameraIDs = queries[0].cameraIDs; + } + const whatSets = uniqWith( + queries.map((query) => query.what), + isEqual, + ); + if (whatSets.length === 1) { + what = queries[0].what; + } + const whereSets = uniqWith( + queries.map((query) => query.where), + isEqual, + ); + if (whereSets.length === 1) { + where = queries[0].where; + } + const favoriteValues = uniqWith( + queries.map((query) => query.favorite), + isEqual, + ); + if (favoriteValues.length === 1) { + favorite = queries[0].favorite; + } + } + + return { + ...(mediaType && { mediaType: mediaType }), + ...(cameraIDs && { cameraIDs: cameraIDs }), + ...(what && { what: what }), + ...(where && { where: where }), + ...(favorite !== undefined && { + favorite: favorite + ? MediaFilterCoreFavoriteSelection.Favorite + : MediaFilterCoreFavoriteSelection.NotFavorite, + }), + }; + } + protected render(): TemplateResult | void { const areEvents = !!( this.view?.query && MediaQueriesClassifier.areEventQueries(this.view.query) @@ -203,6 +302,7 @@ export class FrigateCardMediaFilter extends LitElement { ? !!managerCapabilities?.canFavoriteRecordings : false, }; + const defaults = this._getDefaultsFromView(); return html` `; @@ -249,7 +350,6 @@ export class MediaMetadataController implements ReactiveController { errorToConsole(e as Error); return; } - if (!metadata) { return; } diff --git a/src/components/surround.ts b/src/components/surround.ts index 2b271514..c67d6e43 100644 --- a/src/components/surround.ts +++ b/src/components/surround.ts @@ -88,6 +88,7 @@ export class FrigateCardSurround extends LitElement { this.cameras, this.view, { + targetView: this.view.view, mediaType: this.fetchMedia, }, ); diff --git a/src/utils/media-to-view.ts b/src/utils/media-to-view.ts index 0b79a4cb..7d7d9fe0 100644 --- a/src/utils/media-to-view.ts +++ b/src/utils/media-to-view.ts @@ -45,6 +45,7 @@ export const createViewForEvents = async ( query?: EventMediaQueries; cameraIDs?: Set; mediaType?: ClipsOrSnapshotsOrAll; + targetCameraID?: string; targetView?: FrigateCardView; limit?: number; }, @@ -72,17 +73,11 @@ export const createViewForEvents = async ( return null; } - return executeMediaQueryForView( - element, - hass, - cameraManager, - view, - query, - options?.targetView ?? 'clips', - { - cameraIDs: cameraIDs, - }, - ); + return executeMediaQueryForView(element, hass, cameraManager, view, query, { + cameraIDs: cameraIDs, + targetView: options?.targetView, + targetCameraID: options?.targetCameraID, + }); }; /** @@ -136,6 +131,7 @@ export const createViewForRecordings = async ( options?: { query?: RecordingMediaQueries; cameraIDs?: Set; + targetCameraID?: string; targetView?: 'recording' | 'recordings'; targetTime?: Date; start?: Date; @@ -162,18 +158,12 @@ export const createViewForRecordings = async ( query = new RecordingMediaQueries(recordingQueries); } - return executeMediaQueryForView( - element, - hass, - cameraManager, - view, - query, - options?.targetView ?? 'recordings', - { - cameraIDs: cameraIDs, - ...(options?.targetTime && { targetTime: options.targetTime }), - }, - ); + return executeMediaQueryForView(element, hass, cameraManager, view, query, { + cameraIDs: cameraIDs, + targetView: options?.targetView, + targetCameraID: options?.targetCameraID, + targetTime: options?.targetTime, + }); }; const executeMediaQueryForView = async ( @@ -182,9 +172,10 @@ const executeMediaQueryForView = async ( cameraManager: CameraManager, view: View, query: MediaQueries, - targetView: FrigateCardView, options?: { - cameraIDs: Set; + cameraIDs?: Set; + targetCameraID?: string; + targetView?: FrigateCardView; targetTime?: Date; }, ): Promise => { @@ -214,9 +205,10 @@ const executeMediaQueryForView = async ( return ( view ?.evolve({ - view: targetView, query: query, queryResults: queryResults, + view: options?.targetView, + camera: options?.targetCameraID, }) .mergeInContext(viewerContext) ?? null ); @@ -235,7 +227,7 @@ const executeMediaQueryForView = async ( export const findClosestMediaIndex = ( mediaArray: ViewMedia[], targetTime: Date, - cameraIDs: Set, + cameraIDs?: Set, refPoint?: 'start' | 'end', ): number | null => { let bestMatch: @@ -245,6 +237,10 @@ export const findClosestMediaIndex = ( } | undefined; + if (!cameraIDs) { + return null; + } + for (const [i, media] of mediaArray.entries()) { if (!cameraIDs.has(media.getCameraID())) { continue;