Initial settings for timeline.
This commit is contained in:
@@ -1276,6 +1276,7 @@ export class FrigateCard extends LitElement {
|
|||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.view=${this._view}
|
.view=${this._view}
|
||||||
.cameraConfig=${cameraConfig}
|
.cameraConfig=${cameraConfig}
|
||||||
|
.timelineConfig=${this._getConfig().timeline}
|
||||||
>
|
>
|
||||||
</frigate-card-timeline>`
|
</frigate-card-timeline>`
|
||||||
: ``}
|
: ``}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
CameraConfig,
|
CameraConfig,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
GalleryConfig,
|
GalleryConfig,
|
||||||
|
THUMBNAIL_WIDTH_MAX,
|
||||||
frigateCardConfigDefaults,
|
frigateCardConfigDefaults,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { BrowseMediaUtil } from '../browse-media-util.js';
|
import { BrowseMediaUtil } from '../browse-media-util.js';
|
||||||
@@ -17,8 +18,6 @@ import { stopEventFromActivatingCardWideActions } from '../common.js';
|
|||||||
|
|
||||||
import galleryStyle from '../scss/gallery.scss';
|
import galleryStyle from '../scss/gallery.scss';
|
||||||
|
|
||||||
const MAX_THUMBNAIL_WIDTH = 175;
|
|
||||||
|
|
||||||
@customElement('frigate-card-gallery')
|
@customElement('frigate-card-gallery')
|
||||||
export class FrigateCardGallery extends LitElement {
|
export class FrigateCardGallery extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -124,7 +123,7 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
this._columns = Math.max(
|
this._columns = Math.max(
|
||||||
this.galleryConfig?.min_columns ??
|
this.galleryConfig?.min_columns ??
|
||||||
frigateCardConfigDefaults.event_gallery.min_columns,
|
frigateCardConfigDefaults.event_gallery.min_columns,
|
||||||
Math.ceil(this.clientWidth / MAX_THUMBNAIL_WIDTH),
|
Math.ceil(this.clientWidth / THUMBNAIL_WIDTH_MAX),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+73
-19
@@ -8,12 +8,18 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { DataSet } from 'vis-data/esnext';
|
import { DataSet } from 'vis-data/esnext';
|
||||||
import { HomeAssistant } from 'custom-card-helpers';
|
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 { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref';
|
import { createRef, ref, Ref } from 'lit/directives/ref';
|
||||||
|
|
||||||
import { BrowseMediaUtil } from '../browse-media-util';
|
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 { View } from '../view';
|
||||||
import { contentsChanged, dispatchFrigateCardEvent } from '../common.js';
|
import { contentsChanged, dispatchFrigateCardEvent } from '../common.js';
|
||||||
import { renderProgressIndicator } from './message';
|
import { renderProgressIndicator } from './message';
|
||||||
@@ -38,6 +44,23 @@ export class FrigateCardTimelineEvent extends LitElement {
|
|||||||
@property({ attribute: true })
|
@property({ attribute: true })
|
||||||
protected label?: string;
|
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 {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.thumbnail) {
|
if (!this.thumbnail) {
|
||||||
return;
|
return;
|
||||||
@@ -72,6 +95,17 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected cameraConfig?: CameraConfig;
|
protected cameraConfig?: CameraConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the Home Assistant object.
|
||||||
|
*/
|
||||||
|
set timelineConfig(timelineConfig: TimelineConfig) {
|
||||||
|
this._timelineConfig = timelineConfig;
|
||||||
|
this._setOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _timelineConfig?: TimelineConfig;
|
||||||
|
|
||||||
@state({ hasChanged: contentsChanged })
|
@state({ hasChanged: contentsChanged })
|
||||||
protected _timelineOptions?: TimelineOptions;
|
protected _timelineOptions?: TimelineOptions;
|
||||||
|
|
||||||
@@ -79,14 +113,6 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
protected _timeline?: Timeline;
|
protected _timeline?: Timeline;
|
||||||
protected _boundTimelineSelectHandler = this._timelineSelectHandler.bind(this);
|
protected _boundTimelineSelectHandler = this._timelineSelectHandler.bind(this);
|
||||||
|
|
||||||
protected _resizeObserver: ResizeObserver;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this._resizeObserver = new ResizeObserver(this._setOptions.bind(this));
|
|
||||||
this._setOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master render method.
|
* Master render method.
|
||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
@@ -127,7 +153,6 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
'frigate-card:timeline-select',
|
'frigate-card:timeline-select',
|
||||||
this._boundTimelineSelectHandler,
|
this._boundTimelineSelectHandler,
|
||||||
);
|
);
|
||||||
this._resizeObserver.observe(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,7 +163,6 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
'frigate-card:timeline-select',
|
'frigate-card:timeline-select',
|
||||||
this._boundTimelineSelectHandler,
|
this._boundTimelineSelectHandler,
|
||||||
);
|
);
|
||||||
this._resizeObserver.disconnect();
|
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +196,10 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
media_id=${source.media_content_id}
|
media_id=${source.media_content_id}
|
||||||
thumbnail="${source.thumbnail}"
|
thumbnail="${source.thumbnail}"
|
||||||
label="${source.title}"
|
label="${source.title}"
|
||||||
|
thumbnail_size="${
|
||||||
|
this._timelineConfig?.controls.thumbnails.size_pixels ??
|
||||||
|
frigateCardConfigDefaults.timeline.controls.thumbnails.size_pixels
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
</frigate-card-timeline-event>`;
|
</frigate-card-timeline-event>`;
|
||||||
}
|
}
|
||||||
@@ -204,24 +232,48 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
* Handle timeline resize.
|
* Handle timeline resize.
|
||||||
*/
|
*/
|
||||||
protected _setOptions(): void {
|
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:
|
// Configuration for the Timeline, see:
|
||||||
// https://visjs.github.io/vis-timeline/docs/timeline/#Configuration_Options
|
// https://visjs.github.io/vis-timeline/docs/timeline/#Configuration_Options
|
||||||
this._timelineOptions = {
|
this._timelineOptions = {
|
||||||
cluster: {
|
cluster: thumbnailConfig.clustering_threshold > 0
|
||||||
|
? {
|
||||||
showStipes: true,
|
showStipes: true,
|
||||||
maxItems: 3,
|
// 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%',
|
minHeight: '100%',
|
||||||
maxHeight: '100%',
|
maxHeight: '100%',
|
||||||
margin: {
|
margin: {
|
||||||
item: 50,
|
item: thumbnailConfig.size_pixels - thumbnailConfig.overlap_pixels + gap,
|
||||||
axis: 75 + 10,
|
axis: thumbnailConfig.size_pixels + gap,
|
||||||
},
|
},
|
||||||
xss: {
|
xss: {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
whiteList: {
|
whiteList: {
|
||||||
'frigate-card-timeline-event': ['thumbnail', 'label', 'media_id'],
|
'frigate-card-timeline-event': [
|
||||||
|
'thumbnail',
|
||||||
|
'label',
|
||||||
|
'media_id',
|
||||||
|
'thumbnail_size',
|
||||||
|
],
|
||||||
div: ['title'],
|
div: ['title'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -234,7 +286,9 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
* @param changedProps The changed properties if any.
|
* @param changedProps The changed properties if any.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// 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._timelineRef.value) {
|
||||||
if (this._timeline) {
|
if (this._timeline) {
|
||||||
this._timeline.destroy();
|
this._timeline.destroy();
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
--frigate-card-timeline-thumbnail-size: 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 75px;
|
width: var(--frigate-card-timeline-thumbnail-size);
|
||||||
height: 75px;
|
height: var(--frigate-card-timeline-thumbnail-size);
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
transition: transform 0.2s linear;
|
transition: transform 0.2s linear;
|
||||||
|
|||||||
@@ -4,12 +4,18 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
|
background-color: var(--card-background-color);
|
||||||
|
padding-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.timeline {
|
div.timeline {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vis-text {
|
||||||
|
color: var(--primary-text-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.vis-timeline {
|
.vis-timeline {
|
||||||
border: hidden;
|
border: hidden;
|
||||||
}
|
}
|
||||||
@@ -20,6 +26,7 @@ div.timeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vis-item:hover {
|
.vis-item:hover {
|
||||||
|
// Float icons upwards when the user hovers over them.
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+55
-9
@@ -26,13 +26,13 @@ declare global {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const FRIGATE_CARD_VIEWS_USER_SPECIFIED = [
|
const FRIGATE_CARD_VIEWS_USER_SPECIFIED = [
|
||||||
'live', // Live view.
|
'live',
|
||||||
'clip', // Most recent clip.
|
'clip',
|
||||||
'clips', // Clips gallery.
|
'clips',
|
||||||
'snapshot', // Most recent snapshot.
|
'snapshot',
|
||||||
'snapshots',// Snapshots gallery.
|
'snapshots',
|
||||||
'image', // Static image.
|
'image',
|
||||||
'timeline', // Event timeline.
|
'timeline',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS_USER_SPECIFIED[number];
|
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 {}
|
export class FrigateCardError extends Error {}
|
||||||
|
|
||||||
|
// The maximum width thumbnail Frigate returns
|
||||||
|
export const THUMBNAIL_WIDTH_MAX = 175;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action Types (for "Picture Elements" / Menu)
|
* Action Types (for "Picture Elements" / Menu)
|
||||||
*/
|
*/
|
||||||
@@ -738,6 +741,45 @@ const dimensionsConfigSchema = z
|
|||||||
})
|
})
|
||||||
.default(dimensionsConfigDefault);
|
.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
|
* Configuration overrides
|
||||||
*/
|
*/
|
||||||
@@ -781,6 +823,7 @@ export const frigateCardConfigSchema = z.object({
|
|||||||
image: imageConfigSchema,
|
image: imageConfigSchema,
|
||||||
elements: pictureElementsSchema,
|
elements: pictureElementsSchema,
|
||||||
dimensions: dimensionsConfigSchema,
|
dimensions: dimensionsConfigSchema,
|
||||||
|
timeline: timelineConfigSchema,
|
||||||
|
|
||||||
// Configuration overrides.
|
// Configuration overrides.
|
||||||
overrides: overridesSchema,
|
overrides: overridesSchema,
|
||||||
@@ -804,6 +847,7 @@ export const frigateCardConfigDefaults = {
|
|||||||
event_viewer: viewerConfigDefault,
|
event_viewer: viewerConfigDefault,
|
||||||
event_gallery: galleryConfigDefault,
|
event_gallery: galleryConfigDefault,
|
||||||
image: imageConfigDefault,
|
image: imageConfigDefault,
|
||||||
|
timeline: timelineConfigDefault,
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuButtonSchema = z.discriminatedUnion('type', [
|
const menuButtonSchema = z.discriminatedUnion('type', [
|
||||||
@@ -913,7 +957,8 @@ export const frigateBrowseMediaSourceSchema: z.ZodSchema<BrowseMediaSource> = z.
|
|||||||
children_media_class: z.string().nullable().optional(),
|
children_media_class: z.string().nullable().optional(),
|
||||||
thumbnail: z.string().nullable(),
|
thumbnail: z.string().nullable(),
|
||||||
children: z.array(frigateBrowseMediaSourceSchema).nullable().optional(),
|
children: z.array(frigateBrowseMediaSourceSchema).nullable().optional(),
|
||||||
frigate: z.object({
|
frigate: z
|
||||||
|
.object({
|
||||||
event: z.object({
|
event: z.object({
|
||||||
camera: z.string(),
|
camera: z.string(),
|
||||||
end_time: z.number().nullable(),
|
end_time: z.number().nullable(),
|
||||||
@@ -926,7 +971,8 @@ export const frigateBrowseMediaSourceSchema: z.ZodSchema<BrowseMediaSource> = z.
|
|||||||
top_score: z.number(),
|
top_score: z.number(),
|
||||||
zones: z.string().array(),
|
zones: z.string().array(),
|
||||||
}),
|
}),
|
||||||
}).optional(),
|
})
|
||||||
|
.optional(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user