From e2e332cef184aa12f5187749e73739d58ba27846 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sun, 10 Apr 2022 19:43:33 -0700 Subject: [PATCH] Add window_seconds option to the timeline. --- README.md | 1 + src/components/timeline.ts | 56 +++++++++++++++++++++++++--------- src/const.ts | 1 + src/editor.ts | 2 ++ src/localize/languages/en.json | 1 + src/types.ts | 17 +++++++++-- 6 files changed, 61 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 4fed810d..07d8695e 100644 --- a/README.md +++ b/README.md @@ -404,6 +404,7 @@ timeline: | Option | Default | Overridable | Description | | - | - | - | - | +| `window_seconds` | `3600` | :heavy_multiplication_x: | The length of the default timeline in seconds. By default, 1 hour (`3600` seconds) is shown in the timeline. | | `clustering_threshold` | `3` | :heavy_multiplication_x: | The number of overlapping events to allow prior to clustering/grouping them. Higher numbers cause clustering to happen less frequently. `0` disables clustering entirely.| | `media` | `all` | :heavy_multiplication_x: | Whether to show only events with `clips`, events with `snapshots` or `all` events. When `all` is used, `clips` are favored for events that have both a clip and a snapshot.| | `controls` | | :heavy_multiplication_x: | Configuration for the timeline controls. See below.| diff --git a/src/components/timeline.ts b/src/components/timeline.ts index b00ddb83..b42af0f0 100644 --- a/src/components/timeline.ts +++ b/src/components/timeline.ts @@ -1,4 +1,4 @@ -// TODO: Card actions for timeline +// TODO: Save memory usage by saving thumbnails once. // TODO: Search for TODOs and logging statements. import { @@ -35,6 +35,7 @@ import { MEDIA_TYPE_PLAYLIST, TimelineConfig, FrigateEvent, + frigateCardConfigDefaults, } from '../types'; import { View, ViewContext } from '../view'; import { @@ -596,16 +597,38 @@ export class FrigateCardTimelineCore extends LitElement { * @returns A tuple of start/end date. */ protected _getStartEndFromEvent(event: FrigateEvent): [Date, Date] { - const one_hour = { hours: 1 }; - const start = sub(fromUnixTime(event.start_time), one_hour); - let end: Date; - + const windowSeconds = this._getConfiguredWindowSeconds(); if (event.end_time) { - end = add(fromUnixTime(event.end_time), one_hour); - } else { - end = add(start, one_hour); + if (event.end_time - event.start_time > windowSeconds) { + // If the event is larger than the configured window, only show the most + // recent portion of the event that fits in the window. + return [ + sub(fromUnixTime(event.end_time), {seconds: windowSeconds}), + fromUnixTime(event.end_time) + ]; + } else { + // If the event is shorter than the configured window, center the event + // in the window. + const gap = windowSeconds - (event.end_time - event.start_time); + return [ + sub(fromUnixTime(event.start_time), {seconds: gap / 2}), + add(fromUnixTime(event.end_time), {seconds: gap / 2}), + ] + } } - return [start, end]; + // If there's no end-time yet, place the start-time in the center of the + // time window. + return [ + sub(fromUnixTime(event.start_time), {seconds: windowSeconds / 2}), + add(fromUnixTime(event.start_time), {seconds: windowSeconds / 2}), + ]; + } + + /** + * Get the configured window length in seconds. + */ + protected _getConfiguredWindowSeconds(): number { + return this.timelineConfig?.window_seconds ?? frigateCardConfigDefaults.timeline.window_seconds; } /** @@ -617,9 +640,10 @@ export class FrigateCardTimelineCore extends LitElement { if (event) { return this._getStartEndFromEvent(event); } - const one_hour = { hours: 1 }; const end = new Date(); - const start = sub(end, one_hour); + const start = sub(end, { + seconds: this._getConfiguredWindowSeconds() + }); return [start, end]; } @@ -649,7 +673,6 @@ export class FrigateCardTimelineCore extends LitElement { return { cluster: this._isClustering() ? { - showStipes: true, // It would be better to automatically calculate `maxItems` from the // rendered height of the timeline (or group within the timeline) so // as to not waste vertical space (e.g. after the user changes to @@ -676,7 +699,7 @@ export class FrigateCardTimelineCore extends LitElement { : (false as TimelineOptionsCluster), minHeight: '100%', maxHeight: '100%', - zoomMax: 31 * 24 * 60 * 60 * 1000, + zoomMax: 1 * 24 * 60 * 60 * 1000, zoomMin: 1 * 1000, selectable: true, start: start, @@ -742,7 +765,6 @@ export class FrigateCardTimelineCore extends LitElement { windowStart, windowEnd, ); - this._generateThumbnails(); this._timeline.setSelection(event ? [event.id] : [], { focus: false, @@ -752,6 +774,10 @@ export class FrigateCardTimelineCore extends LitElement { }, }); + // Regenerate the thumbnails after the selection, to allow the new selection + // to be in the generated view. + this._generateThumbnails(); + const context = this.view.context as TimelineViewContext | null; const timelineWindow = this._timeline.getWindow(); @@ -807,7 +833,7 @@ export class FrigateCardTimelineCore extends LitElement { const options = this._getOptions(); if (changedProperties.has('timelineConfig') && this._refTimeline.value && options) { if (this._timeline) { - // TODO this._timeline.setOptions(options); + this._timeline.setOptions(options); } else { this._timeline = new Timeline( this._refTimeline.value, diff --git a/src/const.ts b/src/const.ts index ee5fc637..0bdef699 100644 --- a/src/const.ts +++ b/src/const.ts @@ -89,6 +89,7 @@ export const CONF_IMAGE_REFRESH_SECONDS = `${CONF_IMAGE}.refresh_seconds` as con export const CONF_IMAGE_URL = `${CONF_IMAGE}.url` as const; export const CONF_TIMELINE = 'timeline' as const; +export const CONF_TIMELINE_WINDOW_SECONDS = `${CONF_TIMELINE}.window_seconds` as const; export const CONF_TIMELINE_CLUSTERING_THRESHOLD = `${CONF_TIMELINE}.clustering_threshold` as const; export const CONF_TIMELINE_MEDIA = `${CONF_TIMELINE}.media` as const; export const CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE = `${CONF_TIMELINE}.controls.thumbnails.mode` as const; diff --git a/src/editor.ts b/src/editor.ts index aa4e27ef..f74e9a77 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -74,6 +74,7 @@ import { CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS, CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE, CONF_TIMELINE_MEDIA, + CONF_TIMELINE_WINDOW_SECONDS, CONF_VIEW_CAMERA_SELECT, CONF_VIEW_DEFAULT, CONF_VIEW_TIMEOUT_SECONDS, @@ -1002,6 +1003,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${this._renderOptionSetHeader('timeline')} ${options.timeline.show ? html`
+ ${this._renderNumberInput(CONF_TIMELINE_WINDOW_SECONDS)} ${this._renderNumberInput(CONF_TIMELINE_CLUSTERING_THRESHOLD)} ${this._renderOptionSelector(CONF_TIMELINE_MEDIA, this._timelineMediaTypes)} ${this._renderOptionSelector( diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index a14cf4fb..1813fa37 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -172,6 +172,7 @@ "button_size": "Menu button size (e.g. '40px')" }, "timeline": { + "window_seconds": "The default length of the timeline view in seconds", "clustering_threshold": "The count of events at which they are clustered (0=no clustering)", "media": "The media the timeline displays", "medias": { diff --git a/src/types.ts b/src/types.ts index 687ae243..73a5dfc4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -772,6 +772,7 @@ const dimensionsConfigSchema = z const timelineConfigDefault = { clustering_threshold: 3, media: 'all' as const, + window_seconds: 60 * 60, controls: { thumbnails: { mode: 'left' as const, @@ -783,8 +784,20 @@ const timelineConfigDefault = { }; const timelineConfigSchema = z .object({ - clustering_threshold: z.number().default(timelineConfigDefault.clustering_threshold), - media: z.enum(['all', 'clips', 'snapshots']).default(timelineConfigDefault.media), + clustering_threshold: z + .number() + .optional() + .default(timelineConfigDefault.clustering_threshold), + media: z + .enum(['all', 'clips', 'snapshots']) + .optional() + .default(timelineConfigDefault.media), + window_seconds: z + .number() + .min(1 * 60) + .max(24 * 60 * 60) + .optional() + .default(timelineConfigDefault.window_seconds), controls: z .object({ thumbnails: thumbnailsControlSchema