Add window_seconds option to the timeline.

This commit is contained in:
Dermot Duffy
2022-04-10 19:43:33 -07:00
parent e3e7646fea
commit e2e332cef1
6 changed files with 61 additions and 17 deletions
+1
View File
@@ -404,6 +404,7 @@ timeline:
| Option | Default | Overridable | Description | | 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.| | `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.| | `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.| | `controls` | | :heavy_multiplication_x: | Configuration for the timeline controls. See below.|
+41 -15
View File
@@ -1,4 +1,4 @@
// TODO: Card actions for timeline // TODO: Save memory usage by saving thumbnails once.
// TODO: Search for TODOs and logging statements. // TODO: Search for TODOs and logging statements.
import { import {
@@ -35,6 +35,7 @@ import {
MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_PLAYLIST,
TimelineConfig, TimelineConfig,
FrigateEvent, FrigateEvent,
frigateCardConfigDefaults,
} from '../types'; } from '../types';
import { View, ViewContext } from '../view'; import { View, ViewContext } from '../view';
import { import {
@@ -596,16 +597,38 @@ export class FrigateCardTimelineCore extends LitElement {
* @returns A tuple of start/end date. * @returns A tuple of start/end date.
*/ */
protected _getStartEndFromEvent(event: FrigateEvent): [Date, Date] { protected _getStartEndFromEvent(event: FrigateEvent): [Date, Date] {
const one_hour = { hours: 1 }; const windowSeconds = this._getConfiguredWindowSeconds();
const start = sub(fromUnixTime(event.start_time), one_hour);
let end: Date;
if (event.end_time) { if (event.end_time) {
end = add(fromUnixTime(event.end_time), one_hour); if (event.end_time - event.start_time > windowSeconds) {
} else { // If the event is larger than the configured window, only show the most
end = add(start, one_hour); // 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) { if (event) {
return this._getStartEndFromEvent(event); return this._getStartEndFromEvent(event);
} }
const one_hour = { hours: 1 };
const end = new Date(); const end = new Date();
const start = sub(end, one_hour); const start = sub(end, {
seconds: this._getConfiguredWindowSeconds()
});
return [start, end]; return [start, end];
} }
@@ -649,7 +673,6 @@ export class FrigateCardTimelineCore extends LitElement {
return { return {
cluster: this._isClustering() cluster: this._isClustering()
? { ? {
showStipes: true,
// It would be better to automatically calculate `maxItems` from the // It would be better to automatically calculate `maxItems` from the
// rendered height of the timeline (or group within the timeline) so // rendered height of the timeline (or group within the timeline) so
// as to not waste vertical space (e.g. after the user changes to // 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), : (false as TimelineOptionsCluster),
minHeight: '100%', minHeight: '100%',
maxHeight: '100%', maxHeight: '100%',
zoomMax: 31 * 24 * 60 * 60 * 1000, zoomMax: 1 * 24 * 60 * 60 * 1000,
zoomMin: 1 * 1000, zoomMin: 1 * 1000,
selectable: true, selectable: true,
start: start, start: start,
@@ -742,7 +765,6 @@ export class FrigateCardTimelineCore extends LitElement {
windowStart, windowStart,
windowEnd, windowEnd,
); );
this._generateThumbnails();
this._timeline.setSelection(event ? [event.id] : [], { this._timeline.setSelection(event ? [event.id] : [], {
focus: false, 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 context = this.view.context as TimelineViewContext | null;
const timelineWindow = this._timeline.getWindow(); const timelineWindow = this._timeline.getWindow();
@@ -807,7 +833,7 @@ export class FrigateCardTimelineCore extends LitElement {
const options = this._getOptions(); const options = this._getOptions();
if (changedProperties.has('timelineConfig') && this._refTimeline.value && options) { if (changedProperties.has('timelineConfig') && this._refTimeline.value && options) {
if (this._timeline) { if (this._timeline) {
// TODO this._timeline.setOptions(options); this._timeline.setOptions(options);
} else { } else {
this._timeline = new Timeline( this._timeline = new Timeline(
this._refTimeline.value, this._refTimeline.value,
+1
View File
@@ -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_IMAGE_URL = `${CONF_IMAGE}.url` as const;
export const CONF_TIMELINE = 'timeline' 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_CLUSTERING_THRESHOLD = `${CONF_TIMELINE}.clustering_threshold` as const;
export const CONF_TIMELINE_MEDIA = `${CONF_TIMELINE}.media` 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; export const CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE = `${CONF_TIMELINE}.controls.thumbnails.mode` as const;
+2
View File
@@ -74,6 +74,7 @@ import {
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS, CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE, CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE,
CONF_TIMELINE_MEDIA, CONF_TIMELINE_MEDIA,
CONF_TIMELINE_WINDOW_SECONDS,
CONF_VIEW_CAMERA_SELECT, CONF_VIEW_CAMERA_SELECT,
CONF_VIEW_DEFAULT, CONF_VIEW_DEFAULT,
CONF_VIEW_TIMEOUT_SECONDS, CONF_VIEW_TIMEOUT_SECONDS,
@@ -1002,6 +1003,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderOptionSetHeader('timeline')} ${this._renderOptionSetHeader('timeline')}
${options.timeline.show ${options.timeline.show
? html` <div class="values"> ? html` <div class="values">
${this._renderNumberInput(CONF_TIMELINE_WINDOW_SECONDS)}
${this._renderNumberInput(CONF_TIMELINE_CLUSTERING_THRESHOLD)} ${this._renderNumberInput(CONF_TIMELINE_CLUSTERING_THRESHOLD)}
${this._renderOptionSelector(CONF_TIMELINE_MEDIA, this._timelineMediaTypes)} ${this._renderOptionSelector(CONF_TIMELINE_MEDIA, this._timelineMediaTypes)}
${this._renderOptionSelector( ${this._renderOptionSelector(
+1
View File
@@ -172,6 +172,7 @@
"button_size": "Menu button size (e.g. '40px')" "button_size": "Menu button size (e.g. '40px')"
}, },
"timeline": { "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)", "clustering_threshold": "The count of events at which they are clustered (0=no clustering)",
"media": "The media the timeline displays", "media": "The media the timeline displays",
"medias": { "medias": {
+15 -2
View File
@@ -772,6 +772,7 @@ const dimensionsConfigSchema = z
const timelineConfigDefault = { const timelineConfigDefault = {
clustering_threshold: 3, clustering_threshold: 3,
media: 'all' as const, media: 'all' as const,
window_seconds: 60 * 60,
controls: { controls: {
thumbnails: { thumbnails: {
mode: 'left' as const, mode: 'left' as const,
@@ -783,8 +784,20 @@ const timelineConfigDefault = {
}; };
const timelineConfigSchema = z const timelineConfigSchema = z
.object({ .object({
clustering_threshold: z.number().default(timelineConfigDefault.clustering_threshold), clustering_threshold: z
media: z.enum(['all', 'clips', 'snapshots']).default(timelineConfigDefault.media), .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 controls: z
.object({ .object({
thumbnails: thumbnailsControlSchema thumbnails: thumbnailsControlSchema