Convert timeline to bars rather than icons.
This commit is contained in:
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cycjimmy/jsmpeg-player": "^5.1.1",
|
"@cycjimmy/jsmpeg-player": "^5.1.1",
|
||||||
"@egjs/hammerjs": "^2.0.17",
|
"@egjs/hammerjs": "^2.0.17",
|
||||||
"@lit-labs/task": "^1.0.0",
|
"@lit-labs/task": "^1.1.1",
|
||||||
"@material/image-list": "^13.0.0",
|
"@material/image-list": "^13.0.0",
|
||||||
"@material/mwc-menu": "^0.25.3",
|
"@material/mwc-menu": "^0.25.3",
|
||||||
"@material/rtl": "^13.0.0",
|
"@material/rtl": "^13.0.0",
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ export class BrowseMediaUtil {
|
|||||||
params.clientId,
|
params.clientId,
|
||||||
'event-search',
|
'event-search',
|
||||||
params.mediaType,
|
params.mediaType,
|
||||||
'', // Name/Title to render (not necessary here)
|
|
||||||
|
// If the name field ends in '.all' the integration will return up to 10K events.
|
||||||
|
params.unlimited ? '.all' : '',
|
||||||
params.after ? String(Math.floor(params.after)) : '',
|
params.after ? String(Math.floor(params.after)) : '',
|
||||||
params.before ? String(Math.ceil(params.before)) : '',
|
params.before ? String(Math.ceil(params.before)) : '',
|
||||||
params.cameraName,
|
params.cameraName,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
.cameras=${this._cameras}
|
||||||
.timelineConfig=${this._getConfig().timeline}
|
.timelineConfig=${this._getConfig().timeline}
|
||||||
>
|
>
|
||||||
</frigate-card-timeline>`
|
</frigate-card-timeline>`
|
||||||
|
|||||||
+253
-104
@@ -8,9 +8,14 @@ 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, TimelineOptionsCluster } from 'vis-timeline/esnext';
|
import {
|
||||||
|
DataGroupCollectionType,
|
||||||
|
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.js';
|
||||||
|
|
||||||
import { BrowseMediaUtil } from '../browse-media-util';
|
import { BrowseMediaUtil } from '../browse-media-util';
|
||||||
import {
|
import {
|
||||||
@@ -21,12 +26,20 @@ import {
|
|||||||
frigateCardConfigDefaults,
|
frigateCardConfigDefaults,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { View } from '../view';
|
import { View } from '../view';
|
||||||
import { contentsChanged, dispatchFrigateCardEvent } from '../common.js';
|
import {
|
||||||
import { renderProgressIndicator } from './message';
|
contentsChanged,
|
||||||
|
dispatchErrorMessageEvent,
|
||||||
|
dispatchFrigateCardEvent,
|
||||||
|
getCameraTitle,
|
||||||
|
} from '../common.js';
|
||||||
|
|
||||||
import timelineStyle from '../scss/timeline.scss';
|
import timelineStyle from '../scss/timeline.scss';
|
||||||
import timelineEventStyle from '../scss/timeline-event.scss';
|
import timelineEventStyle from '../scss/timeline-event.scss';
|
||||||
|
|
||||||
|
interface FrigateCardGroupData {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
interface FrigateCardTimelineData {
|
interface FrigateCardTimelineData {
|
||||||
id: string;
|
id: string;
|
||||||
content: string;
|
content: string;
|
||||||
@@ -84,6 +97,95 @@ export class FrigateCardTimelineEvent extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TimelineEventManager {
|
||||||
|
protected _dataset = new DataSet<FrigateCardTimelineData>();
|
||||||
|
|
||||||
|
protected _contentCallback?: (FrigateBrowseMediaSource) => string;
|
||||||
|
protected _tooltipCallback?: (FrigateBrowseMediaSource) => string;
|
||||||
|
|
||||||
|
constructor(params: {
|
||||||
|
contentCallback?: (source: FrigateBrowseMediaSource) => string;
|
||||||
|
tooltipCallback?: (source: FrigateBrowseMediaSource) => string;
|
||||||
|
}) {
|
||||||
|
this._contentCallback = params.contentCallback;
|
||||||
|
this._tooltipCallback = params.tooltipCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
get dataset(): DataSet<FrigateCardTimelineData> {
|
||||||
|
return this._dataset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEmpty(): boolean {
|
||||||
|
return this._dataset.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
this._dataset.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _addMediaSource(camera: string, target: FrigateBrowseMediaSource): void {
|
||||||
|
const items: FrigateCardTimelineData[] = [];
|
||||||
|
target.children?.forEach((child) => {
|
||||||
|
if (child.frigate) {
|
||||||
|
const item = {
|
||||||
|
id: child.media_content_id,
|
||||||
|
group: camera,
|
||||||
|
content: this._contentCallback?.(child) ?? '',
|
||||||
|
title: this._tooltipCallback?.(child) ?? '',
|
||||||
|
start: child.frigate.event.start_time * 1000,
|
||||||
|
};
|
||||||
|
if (child.frigate.event.end_time) {
|
||||||
|
item['end'] = child.frigate.event.end_time * 1000;
|
||||||
|
item['type'] = 'range';
|
||||||
|
} else {
|
||||||
|
item['type'] = 'point';
|
||||||
|
}
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._dataset.update(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchEvents(
|
||||||
|
node: HTMLElement,
|
||||||
|
hass: HomeAssistant & ExtendedHomeAssistant,
|
||||||
|
cameras: Map<string, CameraConfig>,
|
||||||
|
start: Date,
|
||||||
|
end: Date,
|
||||||
|
): Promise<void> {
|
||||||
|
console.info(`fetchEvents: ${start} -> ${end}`);
|
||||||
|
|
||||||
|
// const output = new Map<string, FrigateBrowseMediaSource>();
|
||||||
|
const fetchCameraEvents = async (camera: string): Promise<void> => {
|
||||||
|
const cameraConfig = cameras.get(camera);
|
||||||
|
if (!cameraConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const browseMediaQueryParameters = BrowseMediaUtil.getBrowseMediaQueryParameters(
|
||||||
|
'clips',
|
||||||
|
cameraConfig,
|
||||||
|
);
|
||||||
|
if (!browseMediaQueryParameters) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._addMediaSource(
|
||||||
|
camera,
|
||||||
|
await BrowseMediaUtil.browseMediaQuery(hass, {
|
||||||
|
...browseMediaQueryParameters,
|
||||||
|
unlimited: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return dispatchErrorMessageEvent(node, (e as Error).message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(Array.from(cameras.keys()).map(fetchCameraEvents.bind(this)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@customElement('frigate-card-timeline')
|
@customElement('frigate-card-timeline')
|
||||||
export class FrigateCardTimeline extends LitElement {
|
export class FrigateCardTimeline extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -93,10 +195,10 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
protected view?: Readonly<View>;
|
protected view?: Readonly<View>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected cameraConfig?: CameraConfig;
|
protected cameras?: Map<string, CameraConfig>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the Home Assistant object.
|
* Set the timeline configuration.
|
||||||
*/
|
*/
|
||||||
set timelineConfig(timelineConfig: TimelineConfig) {
|
set timelineConfig(timelineConfig: TimelineConfig) {
|
||||||
this._timelineConfig = timelineConfig;
|
this._timelineConfig = timelineConfig;
|
||||||
@@ -111,86 +213,17 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
|
|
||||||
protected _timelineRef: Ref<HTMLElement> = createRef();
|
protected _timelineRef: Ref<HTMLElement> = createRef();
|
||||||
protected _timeline?: Timeline;
|
protected _timeline?: Timeline;
|
||||||
protected _boundTimelineSelectHandler = this._timelineSelectHandler.bind(this);
|
|
||||||
|
|
||||||
/**
|
protected _events = new TimelineEventManager({
|
||||||
* Master render method.
|
tooltipCallback: this._generateTooltip.bind(this),
|
||||||
* @returns A rendered template.
|
});
|
||||||
*/
|
|
||||||
protected render(): TemplateResult | void {
|
|
||||||
if (!this.hass || !this.view || !this.cameraConfig) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.view.target) {
|
|
||||||
const browseMediaQueryParameters =
|
|
||||||
BrowseMediaUtil.getBrowseMediaQueryParametersOrDispatchError(
|
|
||||||
this,
|
|
||||||
this.view,
|
|
||||||
this.cameraConfig,
|
|
||||||
);
|
|
||||||
if (!browseMediaQueryParameters) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
BrowseMediaUtil.fetchLatestMediaAndDispatchViewChange(
|
|
||||||
this,
|
|
||||||
this.hass,
|
|
||||||
this.view,
|
|
||||||
browseMediaQueryParameters,
|
|
||||||
);
|
|
||||||
return renderProgressIndicator();
|
|
||||||
}
|
|
||||||
return html` <div class="timeline" ${ref(this._timelineRef)}></div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component connected callback.
|
|
||||||
*/
|
|
||||||
connectedCallback(): void {
|
|
||||||
super.connectedCallback();
|
|
||||||
document.addEventListener(
|
|
||||||
'frigate-card:timeline-select',
|
|
||||||
this._boundTimelineSelectHandler,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component disconnected callback.
|
|
||||||
*/
|
|
||||||
disconnectedCallback(): void {
|
|
||||||
document.removeEventListener(
|
|
||||||
'frigate-card:timeline-select',
|
|
||||||
this._boundTimelineSelectHandler,
|
|
||||||
);
|
|
||||||
super.disconnectedCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when an item on the timeline is selected.
|
|
||||||
* @param ev The click event.
|
|
||||||
*/
|
|
||||||
protected _timelineSelectHandler(ev: Event): void {
|
|
||||||
const id = (ev as CustomEvent<string>).detail;
|
|
||||||
const index =
|
|
||||||
this.view?.target?.children?.findIndex((item) => item.media_content_id == id) ??
|
|
||||||
-1;
|
|
||||||
if (index >= 0) {
|
|
||||||
this.view
|
|
||||||
?.evolve({
|
|
||||||
view: 'clip',
|
|
||||||
childIndex: index,
|
|
||||||
})
|
|
||||||
.dispatchChangeEvent(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the content of a single event on the timeline.
|
* Build the content of a single event on the timeline.
|
||||||
* @param source The FrigateBrowseMediaSource object for this event.
|
* @param source The FrigateBrowseMediaSource object for this event.
|
||||||
* @returns A string to include on the timeline.
|
* @returns A string to include on the timeline.
|
||||||
*/
|
*/
|
||||||
protected _buildEventContent(source: FrigateBrowseMediaSource): string {
|
protected _generateTooltip(source: FrigateBrowseMediaSource): string {
|
||||||
return `
|
return `
|
||||||
<frigate-card-timeline-event
|
<frigate-card-timeline-event
|
||||||
media_id=${source.media_content_id}
|
media_id=${source.media_content_id}
|
||||||
@@ -204,28 +237,83 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
</frigate-card-timeline-event>`;
|
</frigate-card-timeline-event>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Master render method.
|
||||||
|
* @returns A rendered template.
|
||||||
|
*/
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this.hass || !this.view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return html` <div class="timeline" ${ref(this._timelineRef)}></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component connected callback.
|
||||||
|
*/
|
||||||
|
connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component disconnected callback.
|
||||||
|
*/
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _timelineRangeHandler(properties: {
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
byUser: boolean;
|
||||||
|
event: Event;
|
||||||
|
}): void {
|
||||||
|
console.info(
|
||||||
|
`Range changed: ${properties.start} -> ${properties.end} [${this._events.dataset.length}]`,
|
||||||
|
);
|
||||||
|
if (this.hass && this.cameras) {
|
||||||
|
// This is not performant in that it refetches all events in the time
|
||||||
|
// range, when some/all may already be fetched. A more optimal approach
|
||||||
|
// would be to only fetch events in time windows that haven't already been
|
||||||
|
// fetched PLUS events that did not previously have an end_time. That's
|
||||||
|
// not trivial to implement, and it's not yet clear it's worth the extra
|
||||||
|
// complexity.
|
||||||
|
this._events.fetchEvents(
|
||||||
|
this,
|
||||||
|
this.hass,
|
||||||
|
this.cameras,
|
||||||
|
properties.start,
|
||||||
|
properties.end,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an object on the timeline is selected.
|
||||||
|
* @param data The data about the selection.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
protected _timelineSelectHandler(data: { items: string[]; event: Event }): void {
|
||||||
|
if (data.items.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make a parent, attach a select bunch of children to it and evolve the view.
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the visjs dataset to render on the timeline.
|
* Build the visjs dataset to render on the timeline.
|
||||||
* @returns The dataset.
|
* @returns The dataset.
|
||||||
*/
|
*/
|
||||||
protected _buildDataset(): DataSet<FrigateCardTimelineData> {
|
protected _getGroups(): DataGroupCollectionType {
|
||||||
const items: FrigateCardTimelineData[] = [];
|
const groups: FrigateCardGroupData[] = [];
|
||||||
|
this.cameras?.forEach((cameraConfig, camera) => {
|
||||||
this.view?.target?.children?.forEach((child) => {
|
groups.push({
|
||||||
if (child.frigate) {
|
id: camera,
|
||||||
const item = {
|
content: getCameraTitle(this.hass, cameraConfig),
|
||||||
id: child.media_content_id,
|
|
||||||
content: this._buildEventContent(child),
|
|
||||||
start: child.frigate.event.start_time * 1000,
|
|
||||||
selectable: false,
|
|
||||||
};
|
|
||||||
if (child.frigate.event.end_time) {
|
|
||||||
//item['end'] = child.frigate.event.end_time * 1000;
|
|
||||||
}
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return new DataSet(items);
|
});
|
||||||
|
return new DataSet(groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -239,12 +327,12 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
const thumbnailConfig =
|
const thumbnailConfig =
|
||||||
this._timelineConfig?.controls.thumbnails ??
|
this._timelineConfig?.controls.thumbnails ??
|
||||||
frigateCardConfigDefaults.timeline.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: thumbnailConfig.clustering_threshold > 0
|
cluster:
|
||||||
|
thumbnailConfig.clustering_threshold > 0
|
||||||
? {
|
? {
|
||||||
showStipes: true,
|
showStipes: true,
|
||||||
// It would be better to automatically calculate `maxItems` from the
|
// It would be better to automatically calculate `maxItems` from the
|
||||||
@@ -257,13 +345,18 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
// created, also does not appear to work as expected.
|
// created, also does not appear to work as expected.
|
||||||
maxItems: thumbnailConfig.clustering_threshold,
|
maxItems: thumbnailConfig.clustering_threshold,
|
||||||
}
|
}
|
||||||
: false as TimelineOptionsCluster,
|
: (false as TimelineOptionsCluster),
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
maxHeight: '100%',
|
maxHeight: '100%',
|
||||||
margin: {
|
tooltip: {
|
||||||
item: thumbnailConfig.size_pixels - thumbnailConfig.overlap_pixels + gap,
|
followMouse: true,
|
||||||
axis: thumbnailConfig.size_pixels + gap,
|
overflowMethod: 'cap',
|
||||||
},
|
},
|
||||||
|
zoomMax: 31 * 24 * 60 * 60 * 1000,
|
||||||
|
zoomMin: 1 * 1000,
|
||||||
|
start: this._getYesterday(),
|
||||||
|
end: this._getToday(),
|
||||||
|
groupHeightMode: 'fixed',
|
||||||
xss: {
|
xss: {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
@@ -275,29 +368,85 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
'thumbnail_size',
|
'thumbnail_size',
|
||||||
],
|
],
|
||||||
div: ['title'],
|
div: ['title'],
|
||||||
|
span: ['style'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get today date object.
|
||||||
|
* @returns A date object for today.
|
||||||
|
*/
|
||||||
|
protected _getToday(): Date {
|
||||||
|
return new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get yesterday date object.
|
||||||
|
* @returns A date object for yesterday.
|
||||||
|
*/
|
||||||
|
protected _getYesterday(): Date {
|
||||||
|
const yesterday = new Date();
|
||||||
|
yesterday.setDate(this._getToday().getDate() - 1);
|
||||||
|
return yesterday;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the component should be updated.
|
||||||
|
* @param _changedProps The changed properties.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected shouldUpdate(_changedProps: PropertyValues): boolean {
|
||||||
|
return !!this.hass && !!this.cameras && this.cameras.size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on the first update.
|
||||||
|
* @param changedProps The changed properties.
|
||||||
|
*/
|
||||||
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
|
||||||
|
if (changedProps.has('cameras')) {
|
||||||
|
this._events.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._events.isEmpty() && this.hass && this.cameras) {
|
||||||
|
// Fetch an initial 1-day worth of events.
|
||||||
|
this._events.fetchEvents(
|
||||||
|
this,
|
||||||
|
this.hass,
|
||||||
|
this.cameras,
|
||||||
|
this._getToday(),
|
||||||
|
this._getYesterday(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the component is updated.
|
* Called when the component is updated.
|
||||||
* @param changedProps The changed properties if any.
|
* @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);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
if (this._timelineRef.value) {
|
if (this._timelineRef.value) {
|
||||||
if (this._timeline) {
|
if (this._timeline) {
|
||||||
this._timeline.destroy();
|
this._timeline.destroy();
|
||||||
|
this._timeline = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._timeline = new Timeline(
|
this._timeline = new Timeline(
|
||||||
this._timelineRef.value,
|
this._timelineRef.value,
|
||||||
this._buildDataset(),
|
this._events.dataset,
|
||||||
|
this._getGroups(),
|
||||||
this._timelineOptions,
|
this._timelineOptions,
|
||||||
);
|
);
|
||||||
|
this._timeline.on('select', this._timelineSelectHandler.bind(this));
|
||||||
|
this._timeline.on('rangechanged', this._timelineRangeHandler.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,13 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
--frigate-card-timeline-thumbnail-size: 75px;
|
--frigate-card-timeline-thumbnail-size: 75px;
|
||||||
|
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: var(--frigate-card-timeline-thumbnail-size);
|
width: var(--frigate-card-timeline-thumbnail-size);
|
||||||
height: var(--frigate-card-timeline-thumbnail-size);
|
height: var(--frigate-card-timeline-thumbnail-size);
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: 5px;
|
|
||||||
transition: transform 0.2s linear;
|
|
||||||
border: 1px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
img:hover {
|
|
||||||
transform: scale(1.04);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-8
@@ -17,12 +17,19 @@ div.timeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vis-timeline {
|
.vis-timeline {
|
||||||
border: hidden;
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vis-labelset .vis-label {
|
||||||
|
// Group labels.
|
||||||
|
color: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vis-item {
|
.vis-item {
|
||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
background: none;
|
background: none;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
background-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vis-item:hover {
|
.vis-item:hover {
|
||||||
@@ -31,7 +38,7 @@ div.timeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vis-item.vis-box {
|
.vis-item.vis-box {
|
||||||
border-style: hidden;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vis-item .vis-item-content {
|
.vis-item .vis-item-content {
|
||||||
@@ -39,13 +46,14 @@ div.timeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vis-item.vis-cluster {
|
.vis-item.vis-cluster {
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-style: dotted;
|
border-style: dotted;
|
||||||
border-radius: 50%;
|
|
||||||
padding: 5px;
|
|
||||||
|
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
background-color: var(--primary-background-color);
|
background-color: var(--primary-background-color);
|
||||||
box-shadow: 0px 0px 20px 5px var(--primary-color);
|
box-shadow: 0px 0px 5px 1px var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.vis-tooltip {
|
||||||
|
padding: 0px;
|
||||||
|
background-color: unset;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
@@ -868,6 +868,7 @@ export interface BrowseMediaQueryParameters {
|
|||||||
zone?: string;
|
zone?: string;
|
||||||
before?: number;
|
before?: number;
|
||||||
after?: number;
|
after?: number;
|
||||||
|
unlimited?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BrowseMediaNeighbors {
|
export interface BrowseMediaNeighbors {
|
||||||
|
|||||||
Reference in New Issue
Block a user