Improvements to updating timeline from view.

This commit is contained in:
Dermot Duffy
2022-09-22 17:44:43 -07:00
parent f11822fc87
commit c60fff243d
4 changed files with 54 additions and 47 deletions
+1 -1
View File
@@ -1030,7 +1030,7 @@ export class FrigateCard extends LitElement {
if (this._cameras && (changedProps.has('_config') || changedProps.has('_cameras'))) { if (this._cameras && (changedProps.has('_config') || changedProps.has('_cameras'))) {
this._timelineDataManager = new TimelineDataManager( this._timelineDataManager = new TimelineDataManager(
this._cameras, this._config.timeline.media, this._config.timeline.show_recordings this._cameras, this._config.timeline.media
) )
} }
} }
+43 -35
View File
@@ -74,16 +74,17 @@ interface FrigateCardGroupData {
} }
interface TimelineRangeChange extends TimelineWindow { interface TimelineRangeChange extends TimelineWindow {
event: Event & { additionalEvent: string }; event: Event & { additionalEvent?: string };
byUser: boolean; byUser: boolean;
} }
interface TimelineViewContext { interface TimelineViewContext {
// The selected timeline window. // Force a particular timeline window rather than taking the time from an
// event / recording.
window?: TimelineWindow; window?: TimelineWindow;
// The date of the last event fetch. // Whether or not to set the timeline window.
dateFetch?: Date; noSetWindow?: boolean;
// Whether or not thumbnails were generated. // Whether or not thumbnails were generated.
generatedThumbnails?: boolean; generatedThumbnails?: boolean;
@@ -453,7 +454,14 @@ export class FrigateCardTimelineCore extends LitElement {
* @param properties * @param properties
*/ */
protected _timelineRangeChangeHandler(properties: TimelineRangeChange): void { 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) { if (this._pointerHeld) {
this._ignoreClick = true; this._ignoreClick = true;
} }
@@ -557,8 +565,11 @@ export class FrigateCardTimelineCore extends LitElement {
...(childIndex !== null && { ...(childIndex !== null && {
childIndex: childIndex, childIndex: childIndex,
}), }),
}) // Whether or not to set the timeline window.
.mergeInContext({
...this._generateTimelineContext({ noSetWindow: true }),
...context,
}) })
.mergeInContext({ ...this._generateTimelineContext(), ...context })
.dispatchChangeEvent(this); .dispatchChangeEvent(this);
} }
} }
@@ -745,7 +756,7 @@ export class FrigateCardTimelineCore extends LitElement {
start: Date; start: Date;
end: Date; end: Date;
byUser: boolean; byUser: boolean;
event: Event; event: Event & { additionalEvent: string };
}): void { }): void {
if (!properties.byUser) { if (!properties.byUser) {
return; return;
@@ -772,7 +783,7 @@ export class FrigateCardTimelineCore extends LitElement {
target: thumbnails?.target ?? null, target: thumbnails?.target ?? null,
childIndex: thumbnails?.childIndex ?? null, childIndex: thumbnails?.childIndex ?? null,
}) })
.mergeInContext(this._generateTimelineContext()) .mergeInContext(this._generateTimelineContext({ noSetWindow: true }))
.dispatchChangeEvent(this); .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 // Hack: Clustering may not update unless the dataset changes, artifically
// update the dataset to ensure the newly selected item cannot be included // 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); this.timelineDataManager?.rewriteItem(event.id);
} }
let contextWindow: TimelineWindow | null = null; if (!this._pointerHeld && !this.view.context?.timeline?.noSetWindow) {
if (!this._pointerHeld) {
// Regenerate the thumbnails after the selection, to allow the new selection // Regenerate the thumbnails after the selection, to allow the new selection
// to be in the generated view. // to be in the generated view.
const context = this.view.context?.timeline; 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 there's a set context window, always move to it.
if (context?.window && !isEqual(context.window, timelineWindow)) { 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) { } 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 start = fromUnixTime(source.start_time);
const end = source.end_time ? fromUnixTime(source.end_time) : 0; const end = source.end_time ? fromUnixTime(source.end_time) : 0;
@@ -1075,21 +1086,13 @@ export class FrigateCardTimelineCore extends LitElement {
if ( if (
start < timelineWindow.start || start < timelineWindow.start ||
start > timelineWindow.end || start > timelineWindow.end ||
(end && (end && (end < timelineWindow.start || end > timelineWindow.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 // Only generate thumbnails if an actual fetch occurred, to avoid getting
// stuck in a loop (the subsequent fetches will not actually fetch since the // stuck in a loop (the subsequent fetches will not actually fetch since the
// data will have been cached). // 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 // Also don't generate thumbnails in mini-timelines (they will already have
// been generated), or if the media child is a recording. // 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(); const thumbnails = this._generateThumbnails();
this.view this.view
?.evolve({ ?.evolve({
target: thumbnails?.target ?? null, target: thumbnails?.target ?? null,
childIndex: thumbnails?.childIndex ?? null, childIndex: thumbnails?.childIndex ?? null,
}) })
.mergeInContext(this._generateTimelineContext(contextWindow)) .mergeInContext(this._generateTimelineContext())
.dispatchChangeEvent(this); .dispatchChangeEvent(this);
} }
} }
/** /**
* Generate the context for timeline views. * Generate the context for timeline views.
* @param addWindow Whether or not to include the timeline window. If `false` * @param options Configure how the context is set.
* the window is preserved if it is already in the context.
* @returns The TimelineViewContext object. * @returns The TimelineViewContext object.
*/ */
protected _generateTimelineContext(window?: TimelineWindow | null): ViewContext { protected _generateTimelineContext(options?: {
noSetWindow?: boolean;
generatedThumbnails?: boolean;
}): ViewContext {
const newContext: TimelineViewContext = { const newContext: TimelineViewContext = {
generatedThumbnails: true, generatedThumbnails: options?.generatedThumbnails ?? true,
}; };
if (this._timeline) {
newContext.window = window ? window : this._timeline.getWindow(); if (options?.noSetWindow) {
} newContext.noSetWindow = options.noSetWindow;
if (this.timelineDataManager?.lastFetchDate) {
newContext.dateFetch = this.timelineDataManager.lastFetchDate;
} }
return { timeline: newContext }; return { timeline: newContext };
} }
@@ -1205,6 +1212,7 @@ export class FrigateCardTimelineCore extends LitElement {
this._dataview = this.timelineDataManager.createDataView( this._dataview = this.timelineDataManager.createDataView(
this._getTimelineCameraIDs(), this._getTimelineCameraIDs(),
!!this.timelineConfig?.show_recordings,
); );
if (this.mini && groups.length === 1) { if (this.mini && groups.length === 1) {
+3 -3
View File
@@ -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: 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: 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: support filtering created dataviews by recordings or mediatype (so storage )
// TODO: dataview refresh instead of rewriteitem?
// TODO: Make minitimeline configurable in the editor // 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 { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
+7 -8
View File
@@ -107,16 +107,13 @@ export class TimelineDataManager {
protected _cameras: Map<string, CameraConfig>; protected _cameras: Map<string, CameraConfig>;
protected _mediaType: TimelineMediaType; protected _mediaType: TimelineMediaType;
protected _recordings: boolean;
constructor( constructor(
cameras: Map<string, CameraConfig>, cameras: Map<string, CameraConfig>,
mediaType: TimelineMediaType, mediaType: TimelineMediaType,
recordings: boolean,
) { ) {
this._cameras = cameras; this._cameras = cameras;
this._mediaType = mediaType; this._mediaType = mediaType;
this._recordings = recordings;
} }
// Get the last event fetch date. // Get the last event fetch date.
@@ -128,10 +125,13 @@ export class TimelineDataManager {
return this._recordingSummary.get(cameraID) ?? null; return this._recordingSummary.get(cameraID) ?? null;
} }
public createDataView(cameraIDs: Set<string>): DataView<FrigateCardTimelineItem> { public createDataView(
cameraIDs: Set<string>,
showRecordings: boolean
): DataView<FrigateCardTimelineItem> {
return new DataView(this._dataset, { return new DataView(this._dataset, {
filter: (item: FrigateCardTimelineItem) => 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) ['video', 'image'].includes(child.media_content_type)
) { ) {
let item = this._dataset.get(event.id); let item = this._dataset.get(event.id);
//const st = fromUnixTime();
if (!item) { if (!item) {
item = { item = {
id: event.id, id: event.id,
@@ -303,8 +302,8 @@ export class TimelineDataManager {
// range. This is because events may change at any point in time // range. This is because events may change at any point in time
// (e.g. a long-running event that ends). // (e.g. a long-running event that ends).
this._fetchEvents(element, hass, this._dateStart, this._dateEnd), this._fetchEvents(element, hass, this._dateStart, this._dateEnd),
...(this._recordings ? [this._fetchRecordingSummary(hass)] : []), this._fetchRecordingSummary(hass),
...(this._recordings && segmentEnd > segmentStart ...(segmentEnd > segmentStart
? [this._fetchRecordingSegments(hass, segmentStart, segmentEnd)] ? [this._fetchRecordingSegments(hass, segmentStart, segmentEnd)]
: []), : []),
]); ]);