diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts index e15a21c5..a54153e4 100644 --- a/src/components/timeline-core.ts +++ b/src/components/timeline-core.ts @@ -595,13 +595,22 @@ export class FrigateCardTimelineCore extends LitElement { this.view && !MediaQueriesClassifier.areRecordingQueries(this.view.query) ) { - ( - await this._createViewWithEventMediaQuery( + const newView = await this._createViewWithEventMediaQuery( this._createEventMediaQuerys({ window: this._timeline.getWindow() }), ) - ) - ?.mergeInContext(this._setWindowInContext()) - ?.dispatchChangeEvent(this); + + // 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._setWindowInContext()) + ?.dispatchChangeEvent(this); + } } } diff --git a/src/utils/basic.ts b/src/utils/basic.ts index e7cabecc..dc89e6d8 100644 --- a/src/utils/basic.ts +++ b/src/utils/basic.ts @@ -181,4 +181,13 @@ export const dayToDate = (day: string): Date => { // Must provide the hour:minute:second on parsing or Javascript will assume // *UTC* midnight. return new Date(`${day}T00:00:00`); -} \ No newline at end of file +}; + +export const isSuperset = (superset: Set, subset: Set) => { + for (const item of subset) { + if (!superset.has(item)) { + return false; + } + } + return true; +}; diff --git a/src/view/media-queries-results.ts b/src/view/media-queries-results.ts index b6de37cd..318ae794 100644 --- a/src/view/media-queries-results.ts +++ b/src/view/media-queries-results.ts @@ -1,4 +1,5 @@ import clone from 'lodash-es/clone.js'; +import { isSuperset } from '../utils/basic.js'; import { ViewMedia } from './media.js'; export class MediaQueriesResults { @@ -21,6 +22,28 @@ export class MediaQueriesResults { return clone(this); } + public isSupersetOf(that: MediaQueriesResults): boolean { + if (!this._results || !that._results) { + return false; + } + + const thisMediaIDs = new Set(this._results.map((media) => media.getID())); + const thatMediaIDs = new Set(that._results.map((media) => media.getID())); + + if ( + !thisMediaIDs || + !thatMediaIDs || + // If either media sets contain a null identifier (i.e. a media item with + // no ID) we must assume this is not a subset as multiple media items may + // reduce to the same null identifier above. + thisMediaIDs.has(null) || + thatMediaIDs.has(null) + ) { + return false; + } + return isSuperset(thisMediaIDs, thatMediaIDs); + } + public getResults(): ViewMedia[] | null { return this._results; } diff --git a/src/view/view.ts b/src/view/view.ts index 48d86d12..e9acae99 100644 --- a/src/view/view.ts +++ b/src/view/view.ts @@ -1,7 +1,6 @@ // Minor / later: // - TODO: ts-prune https://camchenry.com/blog/deleting-dead-code-in-typescript // - TODO: Changing camera in live view, timeline stays the same. -// - TODO: Timeline initial load, then drag, appears to reset? // Gallery: // - TODO: Filter panel expands from right can occasionally 'stick' open.