Initial settings for timeline.

This commit is contained in:
Dermot Duffy
2022-03-19 09:00:21 -07:00
parent 37467eff8c
commit b35b9d0e36
6 changed files with 155 additions and 46 deletions
+1
View File
@@ -1276,6 +1276,7 @@ export class FrigateCard extends LitElement {
.hass=${this._hass}
.view=${this._view}
.cameraConfig=${cameraConfig}
.timelineConfig=${this._getConfig().timeline}
>
</frigate-card-timeline>`
: ``}
+2 -3
View File
@@ -8,6 +8,7 @@ import {
CameraConfig,
ExtendedHomeAssistant,
GalleryConfig,
THUMBNAIL_WIDTH_MAX,
frigateCardConfigDefaults,
} from '../types.js';
import { BrowseMediaUtil } from '../browse-media-util.js';
@@ -17,8 +18,6 @@ import { stopEventFromActivatingCardWideActions } from '../common.js';
import galleryStyle from '../scss/gallery.scss';
const MAX_THUMBNAIL_WIDTH = 175;
@customElement('frigate-card-gallery')
export class FrigateCardGallery extends LitElement {
@property({ attribute: false })
@@ -124,7 +123,7 @@ export class FrigateCardGalleryCore extends LitElement {
this._columns = Math.max(
this.galleryConfig?.min_columns ??
frigateCardConfigDefaults.event_gallery.min_columns,
Math.ceil(this.clientWidth / MAX_THUMBNAIL_WIDTH),
Math.ceil(this.clientWidth / THUMBNAIL_WIDTH_MAX),
);
}
+74 -20
View File
@@ -8,12 +8,18 @@ import {
} from 'lit';
import { DataSet } from 'vis-data/esnext';
import { HomeAssistant } from 'custom-card-helpers';
import { Timeline, TimelineOptions } from 'vis-timeline/esnext';
import { Timeline, TimelineOptions, TimelineOptionsCluster } from 'vis-timeline/esnext';
import { customElement, property, state } from 'lit/decorators.js';
import { createRef, ref, Ref } from 'lit/directives/ref';
import { BrowseMediaUtil } from '../browse-media-util';
import { CameraConfig, ExtendedHomeAssistant, FrigateBrowseMediaSource } from '../types';
import {
CameraConfig,
ExtendedHomeAssistant,
FrigateBrowseMediaSource,
TimelineConfig,
frigateCardConfigDefaults,
} from '../types';
import { View } from '../view';
import { contentsChanged, dispatchFrigateCardEvent } from '../common.js';
import { renderProgressIndicator } from './message';
@@ -38,6 +44,23 @@ export class FrigateCardTimelineEvent extends LitElement {
@property({ attribute: true })
protected label?: string;
@property({ attribute: true, type: Number })
protected thumbnail_size?: number;
/**
* Ensure there is a cached value before an update.
* @param _changedProps The changed properties
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected willUpdate(_changedProps: PropertyValues): void {
if (this.thumbnail_size !== undefined) {
this.style.setProperty(
'--frigate-card-timeline-thumbnail-size',
`${this.thumbnail_size}px`,
);
}
}
protected render(): TemplateResult | void {
if (!this.thumbnail) {
return;
@@ -72,6 +95,17 @@ export class FrigateCardTimeline extends LitElement {
@property({ attribute: false })
protected cameraConfig?: CameraConfig;
/**
* Set the Home Assistant object.
*/
set timelineConfig(timelineConfig: TimelineConfig) {
this._timelineConfig = timelineConfig;
this._setOptions();
}
@state()
protected _timelineConfig?: TimelineConfig;
@state({ hasChanged: contentsChanged })
protected _timelineOptions?: TimelineOptions;
@@ -79,14 +113,6 @@ export class FrigateCardTimeline extends LitElement {
protected _timeline?: Timeline;
protected _boundTimelineSelectHandler = this._timelineSelectHandler.bind(this);
protected _resizeObserver: ResizeObserver;
constructor() {
super();
this._resizeObserver = new ResizeObserver(this._setOptions.bind(this));
this._setOptions();
}
/**
* Master render method.
* @returns A rendered template.
@@ -127,7 +153,6 @@ export class FrigateCardTimeline extends LitElement {
'frigate-card:timeline-select',
this._boundTimelineSelectHandler,
);
this._resizeObserver.observe(this);
}
/**
@@ -138,7 +163,6 @@ export class FrigateCardTimeline extends LitElement {
'frigate-card:timeline-select',
this._boundTimelineSelectHandler,
);
this._resizeObserver.disconnect();
super.disconnectedCallback();
}
@@ -172,6 +196,10 @@ export class FrigateCardTimeline extends LitElement {
media_id=${source.media_content_id}
thumbnail="${source.thumbnail}"
label="${source.title}"
thumbnail_size="${
this._timelineConfig?.controls.thumbnails.size_pixels ??
frigateCardConfigDefaults.timeline.controls.thumbnails.size_pixels
}"
>
</frigate-card-timeline-event>`;
}
@@ -204,24 +232,48 @@ export class FrigateCardTimeline extends LitElement {
* Handle timeline resize.
*/
protected _setOptions(): void {
if (!this._timelineConfig) {
return;
}
const thumbnailConfig =
this._timelineConfig?.controls.thumbnails ??
frigateCardConfigDefaults.timeline.controls.thumbnails;
const gap = 5;
// Configuration for the Timeline, see:
// https://visjs.github.io/vis-timeline/docs/timeline/#Configuration_Options
this._timelineOptions = {
cluster: {
showStipes: true,
maxItems: 3,
},
cluster: thumbnailConfig.clustering_threshold > 0
? {
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
// fullscreen mode). Unfortunately this is not easy to do, as we
// don't know the height of the timeline until after it renders --
// and if we adjust `maxItems` then we can get into an infinite
// resize loop. Adjusting the `maxItems` of a timeline, after it's
// created, also does not appear to work as expected.
maxItems: thumbnailConfig.clustering_threshold,
}
: false as TimelineOptionsCluster,
minHeight: '100%',
maxHeight: '100%',
margin: {
item: 50,
axis: 75 + 10,
item: thumbnailConfig.size_pixels - thumbnailConfig.overlap_pixels + gap,
axis: thumbnailConfig.size_pixels + gap,
},
xss: {
disabled: false,
filterOptions: {
whiteList: {
'frigate-card-timeline-event': ['thumbnail', 'label', 'media_id'],
'frigate-card-timeline-event': [
'thumbnail',
'label',
'media_id',
'thumbnail_size',
],
div: ['title'],
},
},
@@ -234,7 +286,9 @@ export class FrigateCardTimeline extends LitElement {
* @param changedProps The changed properties if any.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected updated(_changedProperties: PropertyValues): void {
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (this._timelineRef.value) {
if (this._timeline) {
this._timeline.destroy();
+4 -2
View File
@@ -2,11 +2,13 @@
display: block;
width: 100%;
height: 100%;
--frigate-card-timeline-thumbnail-size: 75px;
}
img {
width: 75px;
height: 75px;
width: var(--frigate-card-timeline-thumbnail-size);
height: var(--frigate-card-timeline-thumbnail-size);
display: block;
border-radius: 5px;
transition: transform 0.2s linear;
+7
View File
@@ -4,12 +4,18 @@
width: 100%;
height: 100%;
display: block;
background-color: var(--card-background-color);
padding-bottom: 5px;
}
div.timeline {
height: 100%;
}
.vis-text {
color: var(--primary-text-color) !important;
}
.vis-timeline {
border: hidden;
}
@@ -20,6 +26,7 @@ div.timeline {
}
.vis-item:hover {
// Float icons upwards when the user hovers over them.
z-index: 2;
}
+67 -21
View File
@@ -26,13 +26,13 @@ declare global {
*/
const FRIGATE_CARD_VIEWS_USER_SPECIFIED = [
'live', // Live view.
'clip', // Most recent clip.
'clips', // Clips gallery.
'snapshot', // Most recent snapshot.
'snapshots',// Snapshots gallery.
'image', // Static image.
'timeline', // Event timeline.
'live',
'clip',
'clips',
'snapshot',
'snapshots',
'image',
'timeline',
] as const;
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS_USER_SPECIFIED[number];
@@ -59,6 +59,9 @@ export type LiveProvider = typeof LIVE_PROVIDERS[number];
export class FrigateCardError extends Error {}
// The maximum width thumbnail Frigate returns
export const THUMBNAIL_WIDTH_MAX = 175;
/**
* Action Types (for "Picture Elements" / Menu)
*/
@@ -738,6 +741,45 @@ const dimensionsConfigSchema = z
})
.default(dimensionsConfigDefault);
/**
* Timeline configuration section.
*/
const timelineConfigDefault = {
controls: {
thumbnails: {
size_pixels: 75,
overlap_pixels: 25,
clustering_threshold: 3,
},
},
};
const timelineConfigSchema = z
.object({
controls: z
.object({
thumbnails: z
.object({
size_pixels: z
.number()
.min(50)
.max(THUMBNAIL_WIDTH_MAX)
.default(timelineConfigDefault.controls.thumbnails.size_pixels),
overlap_pixels: z
.number()
.min(0)
.max(THUMBNAIL_WIDTH_MAX)
.default(timelineConfigDefault.controls.thumbnails.overlap_pixels),
clustering_threshold: z
.number()
.default(timelineConfigDefault.controls.thumbnails.clustering_threshold),
})
.default(timelineConfigDefault.controls.thumbnails),
})
.default(timelineConfigDefault.controls),
})
.default(timelineConfigDefault);
export type TimelineConfig = z.infer<typeof timelineConfigSchema>;
/**
* Configuration overrides
*/
@@ -781,6 +823,7 @@ export const frigateCardConfigSchema = z.object({
image: imageConfigSchema,
elements: pictureElementsSchema,
dimensions: dimensionsConfigSchema,
timeline: timelineConfigSchema,
// Configuration overrides.
overrides: overridesSchema,
@@ -804,6 +847,7 @@ export const frigateCardConfigDefaults = {
event_viewer: viewerConfigDefault,
event_gallery: galleryConfigDefault,
image: imageConfigDefault,
timeline: timelineConfigDefault,
};
const menuButtonSchema = z.discriminatedUnion('type', [
@@ -913,20 +957,22 @@ export const frigateBrowseMediaSourceSchema: z.ZodSchema<BrowseMediaSource> = z.
children_media_class: z.string().nullable().optional(),
thumbnail: z.string().nullable(),
children: z.array(frigateBrowseMediaSourceSchema).nullable().optional(),
frigate: z.object({
event: z.object({
camera: z.string(),
end_time: z.number().nullable(),
false_positive: z.boolean(),
has_clip: z.boolean(),
has_snapshot: z.boolean(),
id: z.string(),
label: z.string(),
start_time: z.number(),
top_score: z.number(),
zones: z.string().array(),
}),
}).optional(),
frigate: z
.object({
event: z.object({
camera: z.string(),
end_time: z.number().nullable(),
false_positive: z.boolean(),
has_clip: z.boolean(),
has_snapshot: z.boolean(),
id: z.string(),
label: z.string(),
start_time: z.number(),
top_score: z.number(),
zones: z.string().array(),
}),
})
.optional(),
}),
);