diff --git a/src/camera-manager/engine.ts b/src/camera-manager/engine.ts index 2a41e97a..521b665a 100644 --- a/src/camera-manager/engine.ts +++ b/src/camera-manager/engine.ts @@ -23,6 +23,7 @@ import { CameraEndpoints, MediaMetadataQuery, MediaMetadataQueryResultsMap, + EngineOptions, } from './types'; export const CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT = 10000; @@ -58,18 +59,21 @@ export interface CameraManagerEngine { hass: HomeAssistant, cameras: CameraConfigs, query: EventQuery, + engineOptions?: EngineOptions, ): Promise; getRecordings( hass: HomeAssistant, cameras: CameraConfigs, query: RecordingQuery, + engineOptions?: EngineOptions, ): Promise; getRecordingSegments( hass: HomeAssistant, cameras: CameraConfigs, query: RecordingSegmentsQuery, + engineOptions?: EngineOptions, ): Promise; generateMediaFromEvents( @@ -102,12 +106,14 @@ export interface CameraManagerEngine { cameras: CameraConfigs, media: ViewMedia, target: Date, + engineOptions?: EngineOptions, ): Promise; getMediaMetadata( hass: HomeAssistant, cameras: CameraConfigs, query: MediaMetadataQuery, + engineOptions?: EngineOptions, ): Promise; getCameraMetadata( diff --git a/src/camera-manager/frigate/engine-frigate.ts b/src/camera-manager/frigate/engine-frigate.ts index c8e1cc48..96726545 100644 --- a/src/camera-manager/frigate/engine-frigate.ts +++ b/src/camera-manager/frigate/engine-frigate.ts @@ -39,6 +39,7 @@ import { MediaMetadataQuery, MediaMetadataQueryResults, MediaMetadataQueryResultsMap, + EngineOptions, } from '../types'; import { FrigateEventQueryResults, @@ -426,6 +427,7 @@ export class FrigateCameraManagerEngine hass: HomeAssistant, cameras: CameraConfigs, query: EventQuery, + engineOptions?: EngineOptions, ): Promise { const output: EventQueryResultsMap = new Map(); @@ -437,7 +439,8 @@ export class FrigateCameraManagerEngine return; } const instanceQuery = { ...query, cameraIDs: cameraIDs }; - const cachedResult = this._requestCache.get(instanceQuery); + const cachedResult = + engineOptions?.useCache ?? true ? this._requestCache.get(instanceQuery) : null; if (cachedResult) { output.set(query, cachedResult as EventQueryResults); return; @@ -467,7 +470,9 @@ export class FrigateCameraManagerEngine cached: false, }; - this._requestCache.set(query, { ...result, cached: true }, result.expiry); + if (engineOptions?.useCache ?? true) { + this._requestCache.set(query, { ...result, cached: true }, result.expiry); + } output.set(instanceQuery, result); }; @@ -491,6 +496,7 @@ export class FrigateCameraManagerEngine hass: HomeAssistant, cameras: CameraConfigs, query: RecordingQuery, + engineOptions?: EngineOptions, ): Promise { const output: RecordingQueryResultsMap = new Map(); @@ -499,7 +505,8 @@ export class FrigateCameraManagerEngine cameraID: string, ): Promise => { const query = { ...baseQuery, cameraIDs: new Set([cameraID]) }; - const cachedResult = this._requestCache.get(query); + const cachedResult = + engineOptions?.useCache ?? true ? this._requestCache.get(query) : null; if (cachedResult) { output.set(query, cachedResult as RecordingQueryResults); return; @@ -557,7 +564,9 @@ export class FrigateCameraManagerEngine }), cached: false, }; - this._requestCache.set(query, { ...result, cached: true }, result.expiry); + if (engineOptions?.useCache ?? true) { + this._requestCache.set(query, { ...result, cached: true }, result.expiry); + } output.set(query, result); }; @@ -573,6 +582,7 @@ export class FrigateCameraManagerEngine hass: HomeAssistant, cameras: CameraConfigs, query: RecordingSegmentsQuery, + engineOptions?: EngineOptions, ): Promise { const output: RecordingSegmentsQueryResultsMap = new Map(); @@ -595,7 +605,10 @@ export class FrigateCameraManagerEngine // query is different -- the segments won't be). This is since the // volume of data in segment transfers can be high, and the segments can // be used in high frequency situations (e.g. video seeking). - const cachedSegments = this._recordingSegmentsCache.get(cameraID, range); + const cachedSegments = + engineOptions?.useCache ?? true + ? this._recordingSegmentsCache.get(cameraID, range) + : null; if (cachedSegments) { output.set(query, { type: QueryResultsType.RecordingSegments, @@ -615,7 +628,10 @@ export class FrigateCameraManagerEngine }; const segments = await getRecordingSegments(hass, request); - this._recordingSegmentsCache.add(cameraID, range, segments); + + if (engineOptions?.useCache ?? true) { + this._recordingSegmentsCache.add(cameraID, range, segments); + } output.set(query, { type: QueryResultsType.RecordingSegments, @@ -757,6 +773,7 @@ export class FrigateCameraManagerEngine cameras: CameraConfigs, media: ViewMedia, target: Date, + engineOptions?: EngineOptions, ): Promise { const start = media.getStartTime(); const end = media.getEndTime(); @@ -772,7 +789,7 @@ export class FrigateCameraManagerEngine type: QueryType.RecordingSegments, }; - const results = await this.getRecordingSegments(hass, cameras, query); + const results = await this.getRecordingSegments(hass, cameras, query, engineOptions); if (results) { return this._getSeekTimeInSegments( @@ -811,9 +828,10 @@ export class FrigateCameraManagerEngine hass: HomeAssistant, cameras: CameraConfigs, query: MediaMetadataQuery, + engineOptions?: EngineOptions, ): Promise { const output: MediaMetadataQueryResultsMap = new Map(); - if (this._requestCache.has(query)) { + if ((engineOptions?.useCache ?? true) && this._requestCache.has(query)) { const cachedResult = ( this._requestCache.get(query) ); @@ -860,10 +878,15 @@ export class FrigateCameraManagerEngine }; const processRecordings = async (cameraIDs: Set): Promise => { - const recordings = await this.getRecordings(hass, cameras, { - type: QueryType.Recording, - cameraIDs: cameraIDs, - }); + const recordings = await this.getRecordings( + hass, + cameras, + { + type: QueryType.Recording, + cameraIDs: cameraIDs, + }, + engineOptions, + ); if (!recordings) { return; } @@ -902,7 +925,9 @@ export class FrigateCameraManagerEngine cached: false, }; - this._requestCache.set(query, { ...result, cached: true }, result.expiry); + if (engineOptions?.useCache ?? true) { + this._requestCache.set(query, { ...result, cached: true }, result.expiry); + } output.set(query, result); return output; } diff --git a/src/camera-manager/generic/engine-generic.ts b/src/camera-manager/generic/engine-generic.ts index 999665e5..b9022251 100644 --- a/src/camera-manager/generic/engine-generic.ts +++ b/src/camera-manager/generic/engine-generic.ts @@ -9,7 +9,6 @@ import { DataQuery, EventQuery, EventQueryResultsMap, - MediaMetadata, PartialEventQuery, PartialRecordingQuery, PartialRecordingSegmentsQuery, @@ -25,6 +24,7 @@ import { CameraEndpoints, MediaMetadataQuery, MediaMetadataQueryResultsMap, + EngineOptions, } from '../types'; import { getEntityIcon, getEntityTitle } from '../../utils/ha'; import { EntityRegistryManager } from '../../utils/ha/entity-registry'; @@ -71,6 +71,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine { _hass: HomeAssistant, _cameras: CameraConfigs, _query: EventQuery, + _engineOptions?: EngineOptions, ): Promise { return null; } @@ -79,6 +80,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine { _hass: HomeAssistant, _cameras: CameraConfigs, _query: RecordingQuery, + _engineOptions?: EngineOptions, ): Promise { return null; } @@ -87,6 +89,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine { _hass: HomeAssistant, _cameras: CameraConfigs, _query: RecordingSegmentsQuery, + _engineOptions?: EngineOptions, ): Promise { return null; } @@ -134,6 +137,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine { _cameras: CameraConfigs, _media: ViewMedia, _target: Date, + _engineOptions?: EngineOptions, ): Promise { return null; } @@ -142,6 +146,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine { _hass: HomeAssistant, _cameras: CameraConfigs, _query: MediaMetadataQuery, + _engineOptions?: EngineOptions, ): Promise { return null; } diff --git a/src/camera-manager/manager.ts b/src/camera-manager/manager.ts index bfed71cd..94749c2e 100644 --- a/src/camera-manager/manager.ts +++ b/src/camera-manager/manager.ts @@ -33,6 +33,7 @@ import { Engine, MediaMetadataQuery, MediaMetadataQueryResults, + EngineOptions, } from './types.js'; import orderBy from 'lodash-es/orderBy'; import { CameraManagerEngineFactory } from './engine-factory.js'; @@ -361,31 +362,35 @@ export class CameraManager { public async getEvents( hass: HomeAssistant, query: EventQuery | EventQuery[], + engineOptions?: EngineOptions, ): Promise { - return await this._handleQuery(hass, query); + return await this._handleQuery(hass, query, engineOptions); } public async getRecordings( hass: HomeAssistant, query: RecordingQuery | RecordingQuery[], + engineOptions?: EngineOptions, ): Promise { - return await this._handleQuery(hass, query); + return await this._handleQuery(hass, query, engineOptions); } public async getRecordingSegments( hass: HomeAssistant, query: RecordingSegmentsQuery | RecordingSegmentsQuery[], + engineOptions?: EngineOptions, ): Promise { - return await this._handleQuery(hass, query); + return await this._handleQuery(hass, query, engineOptions); } public async executeMediaQueries( hass: HomeAssistant, queries: T[], + engineOptions?: EngineOptions, ): Promise { return this._convertQueryResultsToMedia( hass, - await this._handleQuery(hass, queries), + await this._handleQuery(hass, queries, engineOptions), ); } @@ -394,6 +399,7 @@ export class CameraManager { queries: T[], results: ViewMedia[], direction: 'earlier' | 'later', + engineOptions?: EngineOptions, ): Promise | null> { const getTimeFromResults = (want: 'earliest' | 'latest'): Date | null => { let output: Date | null = null; @@ -446,7 +452,7 @@ export class CameraManager { const newChunkMedia = this._convertQueryResultsToMedia( hass, - await this._handleQuery(hass, newChunkQueries), + await this._handleQuery(hass, newChunkQueries, engineOptions), ); if (!newChunkMedia.length) { @@ -554,6 +560,7 @@ export class CameraManager { protected async _handleQuery( hass: HomeAssistant, query: QT | QT[], + engineOptions?: EngineOptions, ): Promise>> { const _queries = arrayify(query); const results = new Map>(); @@ -573,24 +580,28 @@ export class CameraManager { hass, this._store.getCameras(), query, + engineOptions, )) as Map> | null; } else if (QueryClassifier.isRecordingQuery(query)) { engineResult = (await engine.getRecordings( hass, this._store.getCameras(), query, + engineOptions, )) as Map> | null; } else if (QueryClassifier.isRecordingSegmentsQuery(query)) { engineResult = (await engine.getRecordingSegments( hass, this._store.getCameras(), query, + engineOptions, )) as Map> | null; } else if (QueryClassifier.isMediaMetadataQuery(query)) { engineResult = (await engine.getMediaMetadata( hass, this._store.getCameras(), query, + engineOptions, )) as Map> | null; } diff --git a/src/camera-manager/types.ts b/src/camera-manager/types.ts index 0d0bfd89..d2c9f03d 100644 --- a/src/camera-manager/types.ts +++ b/src/camera-manager/types.ts @@ -130,6 +130,10 @@ export interface CameraEndpoints { export type CameraConfigs = Map; +export interface EngineOptions { + useCache?: boolean; +} + // =========== // Event Query // =========== @@ -148,7 +152,7 @@ export interface EventQuery extends MediaQuery { // Frigate equivalent: sub_label tags?: Set; - + // Frigate equivalent: zone where?: Set; } diff --git a/src/components/gallery.ts b/src/components/gallery.ts index dd0b16ab..d0a847f9 100644 --- a/src/components/gallery.ts +++ b/src/components/gallery.ts @@ -31,17 +31,21 @@ import { MediaQueriesClassifier } from '../view/media-queries-classifier'; import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries'; import { EventQuery, MediaQuery, RecordingQuery } from '../camera-manager/types'; import { MediaQueriesResults } from '../view/media-queries-results'; -import { errorToConsole } from '../utils/basic'; +import { errorToConsole, sleep } from '../utils/basic'; import './media-filter'; import './surround-basic'; import { ViewMedia } from '../view/media'; import { localize } from '../localize/localize'; +import throttle from 'lodash-es/throttle'; +import { classMap } from 'lit/directives/class-map.js'; const GALLERY_MEDIA_FILTER_MENU_ICONS = { closed: 'mdi:filter-cog-outline', open: 'mdi:filter-cog', }; +const MIN_GALLERY_EXTENSION_SECONDS = 0.5; + @customElement('frigate-card-gallery') export class FrigateCardGallery extends LitElement { @property({ attribute: false }) @@ -167,14 +171,42 @@ export class FrigateCardGalleryCore extends LitElement { protected _intersectionObserver: IntersectionObserver; protected _resizeObserver: ResizeObserver; - protected _refLoader: Ref = createRef(); + protected _refLoaderBottom: Ref = createRef(); protected _refSelected: Ref = createRef(); + // Bottom loader: A progress indicator shown in a "cell" (not across) at the + // bottom of the gallery. Once visible this attempts to fetch new content from + // "earlier" (less recently) than the current query. This is rendered by + // default (and once visible, the fetch is triggered after which it is + // re-hidden). @state() - protected _showExtensionLoader = true; + protected _showLoaderBottom = true; + + // Top loader: A progress indicator is shown across the top of the gallery if + // the user is _already_ at the top of the gallery and scrolls upwards. This + // attempts to fetch new content from "later" (more recently) than the current + // query. This is hidden by default. + @state() + protected _showLoaderTop = false; protected _media?: ViewMedia[]; + protected _boundWheelHandler = this._wheelHandler.bind(this); + protected _boundTouchStartHandler = this._touchStartHandler.bind(this); + protected _boundTouchEndHandler = this._touchEndHandler.bind(this); + + // Wheel / touch events may be voluminous, throttle extension calls. + protected _throttleExtendGalleryLater = throttle( + this._extendGallery.bind(this), + MIN_GALLERY_EXTENSION_SECONDS * 1000, + { + leading: true, + trailing: false, + }, + ); + + protected _touchScrollYPosition: number | null = null; + constructor() { super(); this._resizeObserver = new ResizeObserver(this._resizeHandler.bind(this)); @@ -183,12 +215,72 @@ export class FrigateCardGalleryCore extends LitElement { ); } + // Since the scroll event does not fire if the user is already at the top of + // the container, instead we manually use the wheel and touchstart/end events + // to detect "top upwards scrolling" (to trigger an extension of the gallery). + + protected _touchStartHandler(ev: TouchEvent): void { + // Remember the Y touch position on touch start, so that we can calculate if + // the user gestured upwards or downards on touchend. + if (ev.touches.length === 1) { + this._touchScrollYPosition = ev.touches[0].screenY; + } else { + this._touchScrollYPosition = null; + } + } + + protected async _touchEndHandler(ev: TouchEvent): Promise { + if ( + !this.scrollTop && + ev.changedTouches.length === 1 && + this._touchScrollYPosition + ) { + if (ev.changedTouches[0].screenY > this._touchScrollYPosition) { + await this._extendLater(); + } + } + this._touchScrollYPosition = null; + } + + protected async _wheelHandler(ev: WheelEvent): Promise { + if (!this.scrollTop && ev.deltaY < 0) { + await this._extendLater(); + } + } + + protected async _extendLater(): Promise { + const start = new Date(); + this._showLoaderTop = true; + await this._throttleExtendGalleryLater( + 'later', + // Ask the engine to avoid use of cache since the user is explicitly + // looking for the freshest possible data. + false, + ); + const delta = new Date().getTime() - start.getTime(); + if (delta < MIN_GALLERY_EXTENSION_SECONDS * 1000) { + // Hidden gem: "legitimate" (?!) use of sleep() :-) + // These calls can return very quickly even with caching disabled since + // the time window constraints on the query will usually be very narrow + // and the backend can thus very quickly reply. It's often so fast it + // actually looks like a rendering issue where the progress indictor + // barely registers before it's gone again. This optional pause ensures + // there is at least some visual feedback to the user that last long + // enough they can 'feel' the fetch has happened. + await sleep(MIN_GALLERY_EXTENSION_SECONDS - delta / 1000); + } + this._showLoaderTop = false; + } + /** * Component connected callback. */ connectedCallback(): void { super.connectedCallback(); this._resizeObserver.observe(this); + this.addEventListener('wheel', this._boundWheelHandler, { passive: true }); + this.addEventListener('touchstart', this._boundTouchStartHandler, { passive: true }); + this.addEventListener('touchend', this._boundTouchEndHandler); // Request update in order to ensure the intersection observer reconnects // with the loader sentinel. @@ -199,6 +291,9 @@ export class FrigateCardGalleryCore extends LitElement { * Component disconnected callback. */ disconnectedCallback(): void { + this.removeEventListener('wheel', this._boundWheelHandler); + this.removeEventListener('touchstart', this._boundTouchStartHandler); + this.removeEventListener('touchend', this._boundTouchEndHandler); this._resizeObserver.disconnect(); this._intersectionObserver.disconnect(); super.disconnectedCallback(); @@ -232,14 +327,21 @@ export class FrigateCardGalleryCore extends LitElement { protected async _intersectionHandler( entries: IntersectionObserverEntry[], ): Promise { - if (!this.cameraManager || !this.hass || !this.view) { - return; - } if (entries.every((entry) => !entry.isIntersecting)) { return; } - this._showExtensionLoader = false; + this._showLoaderBottom = false; + await this._extendGallery('earlier'); + } + + protected async _extendGallery( + direction: 'earlier' | 'later', + useCache = true, + ): Promise { + if (!this.cameraManager || !this.hass || !this.view) { + return; + } const query = this.view?.query; const rawQueries = query?.getQueries() ?? null; @@ -254,7 +356,10 @@ export class FrigateCardGalleryCore extends LitElement { this.hass, rawQueries, existingMedia, - 'earlier', + direction, + { + useCache: useCache, + }, ); } catch (e) { errorToConsole(e as Error); @@ -272,7 +377,9 @@ export class FrigateCardGalleryCore extends LitElement { this.view ?.evolve({ query: newMediaQueries, - queryResults: new MediaQueriesResults(extension.results), + queryResults: new MediaQueriesResults(extension.results).selectResultIfFound( + (media) => media === this.view?.queryResults?.getSelectedResult(), + ), }) .dispatchChangeEvent(this); } @@ -299,7 +406,9 @@ export class FrigateCardGalleryCore extends LitElement { } } if (changedProps.has('view')) { - this._showExtensionLoader = true; + // 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 ( @@ -330,10 +439,22 @@ export class FrigateCardGalleryCore extends LitElement { const selected = this.view?.queryResults?.getSelectedResult(); return html` + ${this._showLoaderTop + ? html`${renderProgressIndicator({ + cardWideConfig: this.cardWideConfig, + classes: { + top: true, + }, + size: 'small', + })}` + : ''} ${this._media.map( (media, index) => html` `, )} - ${this._showExtensionLoader + ${this._showLoaderBottom ? html`${renderProgressIndicator({ cardWideConfig: this.cardWideConfig, - componentRef: this._refLoader, + componentRef: this._refLoaderBottom, })}` : ''} `; } public updated(changedProps: PropertyValues): void { - if (this._refLoader.value) { + if (this._refLoaderBottom.value) { this._intersectionObserver.disconnect(); - this._intersectionObserver.observe(this._refLoader.value); + this._intersectionObserver.observe(this._refLoaderBottom.value); } // This wait for updateComplete is necessary for the scrolling to work diff --git a/src/components/message.ts b/src/components/message.ts index 66ffd686..83dd9208 100644 --- a/src/components/message.ts +++ b/src/components/message.ts @@ -1,6 +1,6 @@ import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import { classMap } from 'lit/directives/class-map.js'; +import { ClassInfo, classMap } from 'lit/directives/class-map.js'; import { ref, Ref } from 'lit/directives/ref.js'; import { TROUBLESHOOTING_URL } from '../const.js'; import { localize } from '../localize/localize.js'; @@ -29,7 +29,7 @@ export class FrigateCardMessage extends LitElement { dotdotdot: !!this.dotdotdot, }; return html`
-
+
@@ -78,6 +78,8 @@ export class FrigateCardErrorMessage extends LitElement { } } +type FrigateCardProgressIndicatorSize = 'tiny' | 'small' | 'medium' | 'large'; + @customElement('frigate-card-progress-indicator') export class FrigateCardProgressIndicator extends LitElement { @property({ attribute: false }) @@ -86,10 +88,14 @@ export class FrigateCardProgressIndicator extends LitElement { @property({ attribute: false }) public animated = false; + @property({ attribute: false }) + public size: FrigateCardProgressIndicatorSize = 'large'; + protected render(): TemplateResult { return html`
${this.animated - ? html` ` + ? html` + ` : html``} ${this.message ? html`${this.message}` : html``}
`; @@ -119,10 +125,14 @@ export function renderMessage(message: Message): TemplateResult { export function renderProgressIndicator(options?: { message?: string; cardWideConfig?: CardWideConfig; - componentRef?: Ref, + componentRef?: Ref; + classes?: ClassInfo; + size?: FrigateCardProgressIndicatorSize; }): TemplateResult { return html` , subset: Set) => { } return true; }; + +// Usage of this function needs to be justified with a comment. +export const sleep = async (seconds: number) => { + await new Promise((r) => setTimeout(r, seconds * 1000)); +}; diff --git a/src/utils/debug.ts b/src/utils/debug.ts index 9316ee09..42060166 100644 --- a/src/utils/debug.ts +++ b/src/utils/debug.ts @@ -5,12 +5,3 @@ export const log = (cardWideConfig?: CardWideConfig, ...args: unknown[]) => { console.debug(...args); } }; - -/** - * For debug purposes only. - * @param seconds - */ -// ts-prune-ignore-next -export const sleep = async (seconds: number) => { - await new Promise((r) => setTimeout(r, seconds * 1000)); -}; diff --git a/src/utils/media-to-view.ts b/src/utils/media-to-view.ts index e93ea145..274875e2 100644 --- a/src/utils/media-to-view.ts +++ b/src/utils/media-to-view.ts @@ -16,6 +16,8 @@ import { errorToConsole } from './basic'; import { MediaQuery } from '../camera-manager/types'; import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const'; +type ResultSelectType = 'latest' | 'time' | 'none'; + export const changeViewToRecentEventsForCameraAndDependents = async ( element: HTMLElement, hass: HomeAssistant, @@ -25,6 +27,7 @@ export const changeViewToRecentEventsForCameraAndDependents = async ( options?: { mediaType?: ClipsOrSnapshotsOrAll; targetView?: FrigateCardView; + select?: ResultSelectType; }, ): Promise => { const cameraIDs = getAllDependentCameras(cameraManager, view.camera); @@ -42,6 +45,7 @@ export const changeViewToRecentEventsForCameraAndDependents = async ( ( await executeMediaQueryForView(element, hass, cameraManager, view, queries, { targetView: options?.targetView, + select: options?.select, }) )?.dispatchChangeEvent(element); }; @@ -81,6 +85,7 @@ export const changeViewToRecentRecordingForCameraAndDependents = async ( view: View, options?: { targetView?: 'recording' | 'recordings'; + select?: ResultSelectType; }, ): Promise => { const cameraIDs = getAllDependentCameras(cameraManager, view.camera); @@ -100,6 +105,7 @@ export const changeViewToRecentRecordingForCameraAndDependents = async ( ( await executeMediaQueryForView(element, hass, cameraManager, view, queries, { targetView: options?.targetView, + select: options?.select, }) )?.dispatchChangeEvent(element); }; @@ -133,6 +139,7 @@ export const executeMediaQueryForView = async ( targetCameraID?: string; targetView?: FrigateCardView; targetTime?: Date; + select?: ResultSelectType; }, ): Promise => { let mediaArray: ViewMedia[] | null; @@ -153,12 +160,16 @@ export const executeMediaQueryForView = async ( if (!mediaArray) { return null; } - // Select the last item by default (which is the most recent). - const selectedIndex = mediaArray.length ? mediaArray.length - 1 : undefined; - const queryResults = new MediaQueriesResults(mediaArray, selectedIndex); + + const queryResults = new MediaQueriesResults( + mediaArray, + options?.select === 'latest' && mediaArray.length + ? mediaArray.length - 1 + : undefined, + ); let viewerContext: ViewContext | undefined = {}; - if (options?.targetTime) { + if (options?.select === 'time' && options?.targetTime) { queryResults.selectBestResult((media) => findClosestMediaIndex(media, options.targetTime as Date), ); diff --git a/src/view/media-queries-results.ts b/src/view/media-queries-results.ts index 318ae794..e0d9f5d4 100644 --- a/src/view/media-queries-results.ts +++ b/src/view/media-queries-results.ts @@ -11,7 +11,9 @@ export class MediaQueriesResults { if (results) { this.setResults(results); } - this.selectResult(selectedIndex ?? 0); + if (selectedIndex !== undefined) { + this.selectResult(selectedIndex); + } } public clone(): MediaQueriesResults {