diff --git a/src/card-controller/view/factory.ts b/src/card-controller/view/factory.ts index 9f43d5f3..5a0f6b8e 100644 --- a/src/card-controller/view/factory.ts +++ b/src/card-controller/view/factory.ts @@ -7,6 +7,7 @@ import { } from '../../config/types'; import { localize } from '../../localize/localize'; import { ClipsOrSnapshotsOrAll } from '../../types'; +import { MediaQueriesClassifier } from '../../view/media-queries-classifier'; import { View, ViewParameters } from '../../view/view'; import { getCameraIDsForViewName } from '../../view/view-to-cameras'; import { CardViewAPI } from '../types'; @@ -221,7 +222,15 @@ export class ViewFactory { const switchingToGalleryFromViewer = baseView?.isViewerView() && view.isGalleryView(); - if (switchingToGalleryFromViewer && baseView?.query && baseView?.queryResults) { + const alreadyHasMatchingQuery = + mediaType === MediaQueriesClassifier.getMediaType(baseView?.query); + + if ( + switchingToGalleryFromViewer && + alreadyHasMatchingQuery && + baseView?.query && + baseView?.queryResults + ) { // If the user is currently using the viewer, and then switches to the // gallery we make an attempt to keep the query/queryResults the same so // the gallery can be used to click back and forth to the viewer, and the diff --git a/src/view/media-queries-classifier.ts b/src/view/media-queries-classifier.ts index 9c35cb23..4a6dd0b4 100644 --- a/src/view/media-queries-classifier.ts +++ b/src/view/media-queries-classifier.ts @@ -1,6 +1,8 @@ import { EventMediaQueries, MediaQueries, RecordingMediaQueries } from './media-queries'; export type MediaQueriesType = 'event' | 'recording'; +type MediaType = 'clips' | 'snapshots' | 'recordings'; + export class MediaQueriesClassifier { public static areEventQueries( queries?: MediaQueries | null, @@ -14,6 +16,20 @@ export class MediaQueriesClassifier { return queries instanceof RecordingMediaQueries; } + public static areClipsQueries(queries?: MediaQueries | null): boolean { + return ( + this.areEventQueries(queries) && + !!queries?.getQueries()?.every((query) => query.hasClip) + ); + } + + public static areSnapshotQueries(queries?: MediaQueries | null): boolean { + return ( + this.areEventQueries(queries) && + !!queries?.getQueries()?.every((query) => query.hasSnapshot) + ); + } + public static getQueriesType(queries?: MediaQueries | null): MediaQueriesType | null { return this.areEventQueries(queries) ? 'event' @@ -21,4 +37,14 @@ export class MediaQueriesClassifier { ? 'recording' : null; } + + public static getMediaType(queries?: MediaQueries | null): MediaType | null { + return this.areClipsQueries(queries) + ? 'clips' + : this.areSnapshotQueries(queries) + ? 'snapshots' + : this.areRecordingQueries(queries) + ? 'recordings' + : null; + } } diff --git a/src/view/view-to-cameras.ts b/src/view/view-to-cameras.ts index 449f13f7..2efbf40b 100644 --- a/src/view/view-to-cameras.ts +++ b/src/view/view-to-cameras.ts @@ -1,4 +1,5 @@ import { CameraManager } from '../camera-manager/manager'; +import { CapabilitySearchOptions } from '../camera-manager/types'; import { FrigateCardView } from '../config/types'; /** @@ -10,6 +11,10 @@ export const getCameraIDsForViewName = ( viewName: FrigateCardView, cameraID?: string, ): Set => { + const capabilityMatchAnyMedia: CapabilitySearchOptions = { + anyCapabilities: ['clips', 'snapshots', 'recordings'], + }; + switch (viewName) { case 'image': case 'diagnostics': @@ -35,17 +40,15 @@ export const getCameraIDsForViewName = ( : cameraManager.getStore().getCameraIDsWithCapability(capability); case 'timeline': - return cameraManager.getStore().getCameraIDsWithCapability({ - anyCapabilities: ['clips', 'snapshots', 'recordings'], - }); + return cameraManager + .getStore() + .getCameraIDsWithCapability(capabilityMatchAnyMedia); case 'media': return cameraID - ? cameraManager.getStore().getAllDependentCameras(cameraID, { - anyCapabilities: ['clips', 'snapshots', 'recordings'], - }) - : cameraManager.getStore().getCameraIDsWithCapability({ - anyCapabilities: ['clips', 'snapshots', 'recordings'], - }); + ? cameraManager + .getStore() + .getAllDependentCameras(cameraID, capabilityMatchAnyMedia) + : cameraManager.getStore().getCameraIDsWithCapability(capabilityMatchAnyMedia); } }; diff --git a/tests/card-controller/view/factory.test.ts b/tests/card-controller/view/factory.test.ts index 7e84115c..b837e39b 100644 --- a/tests/card-controller/view/factory.test.ts +++ b/tests/card-controller/view/factory.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; +import { QueryType } from '../../../src/camera-manager/types'; import { ViewFactory } from '../../../src/card-controller/view/factory'; import { QueryExecutor } from '../../../src/card-controller/view/query-executor'; import { ViewModifier } from '../../../src/card-controller/view/types'; @@ -618,7 +619,13 @@ describe('getViewByParametersWithNewQuery', () => { const baseView = new View({ view: 'media', camera: 'camera.office', - query: new EventMediaQueries(), + query: new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['camera.office']), + hasClip: true, + }, + ]), queryResults: new MediaQueriesResults(), }); diff --git a/tests/view/media-queries-classifier.test.ts b/tests/view/media-queries-classifier.test.ts index 0b96608b..0b2fe4ba 100644 --- a/tests/view/media-queries-classifier.test.ts +++ b/tests/view/media-queries-classifier.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { EventMediaQueries, RecordingMediaQueries } from '../../src/view/media-queries'; import { MediaQueriesClassifier } from '../../src/view/media-queries-classifier'; +import { QueryType } from '../../src/camera-manager/types'; describe('MediaQueriesClassifier', () => { it('areEventQueries', () => { @@ -26,4 +27,94 @@ describe('MediaQueriesClassifier', () => { ); expect(MediaQueriesClassifier.getQueriesType()).toBeNull(); }); + + it('areClipsQueries', () => { + expect( + MediaQueriesClassifier.areClipsQueries( + new EventMediaQueries([ + { type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true }, + ]), + ), + ).toBeTruthy(); + expect( + MediaQueriesClassifier.areClipsQueries( + new EventMediaQueries([ + { type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true }, + { type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: false }, + ]), + ), + ).toBeFalsy(); + expect( + MediaQueriesClassifier.areClipsQueries( + new EventMediaQueries([ + { type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true }, + { type: QueryType.Event, cameraIDs: new Set(['camera']) }, + ]), + ), + ).toBeFalsy(); + }); + + it('areSnapshotQueries', () => { + expect( + MediaQueriesClassifier.areSnapshotQueries( + new EventMediaQueries([ + { type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: true }, + ]), + ), + ).toBeTruthy(); + expect( + MediaQueriesClassifier.areSnapshotQueries( + new EventMediaQueries([ + { type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: true }, + { type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: false }, + ]), + ), + ).toBeFalsy(); + expect( + MediaQueriesClassifier.areSnapshotQueries( + new EventMediaQueries([ + { type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: true }, + { type: QueryType.Event, cameraIDs: new Set(['camera']) }, + ]), + ), + ).toBeFalsy(); + }); + + it('getMediaType', () => { + expect( + MediaQueriesClassifier.getMediaType( + new EventMediaQueries([ + { type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: true }, + { type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: true }, + ]), + ), + ).toBe('snapshots'); + + expect( + MediaQueriesClassifier.getMediaType( + new EventMediaQueries([ + { type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true }, + { type: QueryType.Event, cameraIDs: new Set(['camera']), hasClip: true }, + ]), + ), + ).toBe('clips'); + + expect( + MediaQueriesClassifier.getMediaType( + new RecordingMediaQueries([ + { type: QueryType.Recording, cameraIDs: new Set(['camera']) }, + { type: QueryType.Recording, cameraIDs: new Set(['camera']) }, + ]), + ), + ).toBe('recordings'); + + expect( + MediaQueriesClassifier.getMediaType( + new EventMediaQueries([ + { type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: true }, + { type: QueryType.Event, cameraIDs: new Set(['camera']), hasSnapshot: false }, + ]), + ), + ).toBeNull(); + }); });