From c60fff243d867f378e53585c08e71db000276091 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Mon, 19 Sep 2022 07:40:55 -0700 Subject: [PATCH] Improvements to updating timeline from view. --- src/card.ts | 2 +- src/components/timeline-core.ts | 78 ++++++++++++++++-------------- src/components/timeline.ts | 6 +-- src/utils/timeline-data-manager.ts | 15 +++--- 4 files changed, 54 insertions(+), 47 deletions(-) diff --git a/src/card.ts b/src/card.ts index f595f678..1ea14574 100644 --- a/src/card.ts +++ b/src/card.ts @@ -1030,7 +1030,7 @@ export class FrigateCard extends LitElement { if (this._cameras && (changedProps.has('_config') || changedProps.has('_cameras'))) { this._timelineDataManager = new TimelineDataManager( - this._cameras, this._config.timeline.media, this._config.timeline.show_recordings + this._cameras, this._config.timeline.media ) } } diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts index ff05bc1d..ac18e7e3 100644 --- a/src/components/timeline-core.ts +++ b/src/components/timeline-core.ts @@ -74,16 +74,17 @@ interface FrigateCardGroupData { } interface TimelineRangeChange extends TimelineWindow { - event: Event & { additionalEvent: string }; + event: Event & { additionalEvent?: string }; byUser: boolean; } interface TimelineViewContext { - // The selected timeline window. + // Force a particular timeline window rather than taking the time from an + // event / recording. window?: TimelineWindow; - // The date of the last event fetch. - dateFetch?: Date; + // Whether or not to set the timeline window. + noSetWindow?: boolean; // Whether or not thumbnails were generated. generatedThumbnails?: boolean; @@ -453,7 +454,14 @@ export class FrigateCardTimelineCore extends LitElement { * @param properties */ protected _timelineRangeChangeHandler(properties: TimelineRangeChange): void { - if (this._timeline && properties.byUser) { + if ( + this._timeline && + properties.byUser && + // Do not adjust select children or seek during zoom events. + properties.event.type !== 'wheel' && + properties.event.additionalEvent !== 'pinchin' && + properties.event.additionalEvent !== 'pinchout' + ) { if (this._pointerHeld) { this._ignoreClick = true; } @@ -557,8 +565,11 @@ export class FrigateCardTimelineCore extends LitElement { ...(childIndex !== null && { childIndex: childIndex, }), + }) // Whether or not to set the timeline window. + .mergeInContext({ + ...this._generateTimelineContext({ noSetWindow: true }), + ...context, }) - .mergeInContext({ ...this._generateTimelineContext(), ...context }) .dispatchChangeEvent(this); } } @@ -745,7 +756,7 @@ export class FrigateCardTimelineCore extends LitElement { start: Date; end: Date; byUser: boolean; - event: Event; + event: Event & { additionalEvent: string }; }): void { if (!properties.byUser) { return; @@ -772,7 +783,7 @@ export class FrigateCardTimelineCore extends LitElement { target: thumbnails?.target ?? null, childIndex: thumbnails?.childIndex ?? null, }) - .mergeInContext(this._generateTimelineContext()) + .mergeInContext(this._generateTimelineContext({ noSetWindow: true })) .dispatchChangeEvent(this); } }); @@ -1049,15 +1060,15 @@ export class FrigateCardTimelineCore extends LitElement { }, }); - if (event && this._isClustering()) { + if (!this._pointerHeld && event && this._isClustering()) { // Hack: Clustering may not update unless the dataset changes, artifically // update the dataset to ensure the newly selected item cannot be included - // in a cluster. + // in a cluster. Only do this when the pointer is not held to avoid + // interrupting the user and to make the timeline smoother. this.timelineDataManager?.rewriteItem(event.id); } - let contextWindow: TimelineWindow | null = null; - if (!this._pointerHeld) { + if (!this._pointerHeld && !this.view.context?.timeline?.noSetWindow) { // Regenerate the thumbnails after the selection, to allow the new selection // to be in the generated view. const context = this.view.context?.timeline; @@ -1065,9 +1076,9 @@ export class FrigateCardTimelineCore extends LitElement { // If there's a set context window, always move to it. if (context?.window && !isEqual(context.window, timelineWindow)) { - contextWindow = {start: context.window.start, end: context.window.end}; + this._timeline.setWindow(context.window.start, context.window.end); } else if (event || recording) { - const source = event ?? recording as FrigateEvent | FrigateRecording; + const source = event ?? (recording as FrigateEvent | FrigateRecording); const start = fromUnixTime(source.start_time); const end = source.end_time ? fromUnixTime(source.end_time) : 0; @@ -1075,21 +1086,13 @@ export class FrigateCardTimelineCore extends LitElement { if ( start < timelineWindow.start || start > timelineWindow.end || - (end && - (end < timelineWindow.start || end > timelineWindow.end)) + (end && (end < timelineWindow.start || end > timelineWindow.end)) ) { - contextWindow = {start: windowStart, end: windowEnd}; + this._timeline.setWindow(windowStart, windowEnd); } - } else { - // Otherwise just the default window. - contextWindow = {start: windowStart, end: windowEnd}; } } - if (contextWindow) { - this._timeline.setWindow(contextWindow.start, contextWindow.end); - } - // Only generate thumbnails if an actual fetch occurred, to avoid getting // stuck in a loop (the subsequent fetches will not actually fetch since the // data will have been cached). @@ -1102,33 +1105,37 @@ export class FrigateCardTimelineCore extends LitElement { // // Also don't generate thumbnails in mini-timelines (they will already have // been generated), or if the media child is a recording. - if ((fetched || !this.view.context?.timeline?.generatedThumbnails) && !this.mini && !recording) { + if ( + (fetched || !this.view.context?.timeline?.generatedThumbnails) && + !this.mini && + !recording + ) { const thumbnails = this._generateThumbnails(); this.view ?.evolve({ target: thumbnails?.target ?? null, childIndex: thumbnails?.childIndex ?? null, }) - .mergeInContext(this._generateTimelineContext(contextWindow)) + .mergeInContext(this._generateTimelineContext()) .dispatchChangeEvent(this); } } /** * Generate the context for timeline views. - * @param addWindow Whether or not to include the timeline window. If `false` - * the window is preserved if it is already in the context. + * @param options Configure how the context is set. * @returns The TimelineViewContext object. */ - protected _generateTimelineContext(window?: TimelineWindow | null): ViewContext { + protected _generateTimelineContext(options?: { + noSetWindow?: boolean; + generatedThumbnails?: boolean; + }): ViewContext { const newContext: TimelineViewContext = { - generatedThumbnails: true, + generatedThumbnails: options?.generatedThumbnails ?? true, }; - if (this._timeline) { - newContext.window = window ? window : this._timeline.getWindow(); - } - if (this.timelineDataManager?.lastFetchDate) { - newContext.dateFetch = this.timelineDataManager.lastFetchDate; + + if (options?.noSetWindow) { + newContext.noSetWindow = options.noSetWindow; } return { timeline: newContext }; } @@ -1205,6 +1212,7 @@ export class FrigateCardTimelineCore extends LitElement { this._dataview = this.timelineDataManager.createDataView( this._getTimelineCameraIDs(), + !!this.timelineConfig?.show_recordings, ); if (this.mini && groups.length === 1) { diff --git a/src/components/timeline.ts b/src/components/timeline.ts index 26e39591..b660cabf 100644 --- a/src/components/timeline.ts +++ b/src/components/timeline.ts @@ -1,10 +1,10 @@ // TODO: When a media viewer is first loaded the selected child won't work (because the underlying carousel has not yet rendered) -// TODO: thumbnails in drawers don't work. + +// TODO: unselect in mini timeline should not cause media viewer to render nothing // TODO: delete segments if not in summary? is this actually necessary? could it create gaps in data? better off stopping access via summary? // TODO: support filtering created dataviews by recordings or mediatype (so storage ) -// TODO: dataview refresh instead of rewriteitem? // TODO: Make minitimeline configurable in the editor -// TODO: Is it really useful to select the children in the main timeline view on range change? +// TODO: Make performance okay for 2+ days of events import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { customElement, property } from 'lit/decorators.js'; diff --git a/src/utils/timeline-data-manager.ts b/src/utils/timeline-data-manager.ts index 1e2ec918..e00df5f3 100644 --- a/src/utils/timeline-data-manager.ts +++ b/src/utils/timeline-data-manager.ts @@ -107,16 +107,13 @@ export class TimelineDataManager { protected _cameras: Map; protected _mediaType: TimelineMediaType; - protected _recordings: boolean; constructor( cameras: Map, mediaType: TimelineMediaType, - recordings: boolean, ) { this._cameras = cameras; this._mediaType = mediaType; - this._recordings = recordings; } // Get the last event fetch date. @@ -128,10 +125,13 @@ export class TimelineDataManager { return this._recordingSummary.get(cameraID) ?? null; } - public createDataView(cameraIDs: Set): DataView { + public createDataView( + cameraIDs: Set, + showRecordings: boolean + ): DataView { return new DataView(this._dataset, { filter: (item: FrigateCardTimelineItem) => - !!item.group && cameraIDs.has(String(item.group)), + !!item.group && cameraIDs.has(String(item.group)) && (showRecordings || item.type !== 'background') }); } @@ -175,7 +175,6 @@ export class TimelineDataManager { ['video', 'image'].includes(child.media_content_type) ) { let item = this._dataset.get(event.id); - //const st = fromUnixTime(); if (!item) { item = { id: event.id, @@ -303,8 +302,8 @@ export class TimelineDataManager { // range. This is because events may change at any point in time // (e.g. a long-running event that ends). this._fetchEvents(element, hass, this._dateStart, this._dateEnd), - ...(this._recordings ? [this._fetchRecordingSummary(hass)] : []), - ...(this._recordings && segmentEnd > segmentStart + this._fetchRecordingSummary(hass), + ...(segmentEnd > segmentStart ? [this._fetchRecordingSegments(hass, segmentStart, segmentEnd)] : []), ]);