Show thumbnails as tooltips.
This commit is contained in:
@@ -179,6 +179,7 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
|||||||
.target=${parent}
|
.target=${parent}
|
||||||
.childIndex=${childIndex}
|
.childIndex=${childIndex}
|
||||||
?details=${this._config?.show_details}
|
?details=${this._config?.show_details}
|
||||||
|
?controls=${true}
|
||||||
thumbnail_size=${ifDefined(this._config?.size)}
|
thumbnail_size=${ifDefined(this._config?.size)}
|
||||||
class="${classMap(classes)}"
|
class="${classMap(classes)}"
|
||||||
@click=${(ev) => {
|
@click=${(ev) => {
|
||||||
|
|||||||
+54
-18
@@ -2,7 +2,7 @@ import { CSSResult, TemplateResult, html, unsafeCSS, LitElement } from 'lit';
|
|||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { format, fromUnixTime } from 'date-fns';
|
import { format, fromUnixTime } from 'date-fns';
|
||||||
|
|
||||||
import type { FrigateBrowseMediaSource } from '../types.js';
|
import type { FrigateBrowseMediaSource, FrigateEvent } from '../types.js';
|
||||||
import { View } from '../view.js';
|
import { View } from '../view.js';
|
||||||
import {
|
import {
|
||||||
getEventDurationString,
|
getEventDurationString,
|
||||||
@@ -15,6 +15,20 @@ import thumbnailStyle from '../scss/thumbnail.scss';
|
|||||||
|
|
||||||
@customElement('frigate-card-thumbnail')
|
@customElement('frigate-card-thumbnail')
|
||||||
export class FrigateCardThumbnail extends LitElement {
|
export class FrigateCardThumbnail extends LitElement {
|
||||||
|
@property({ attribute: true, type: Boolean })
|
||||||
|
public details = false;
|
||||||
|
|
||||||
|
@property({ attribute: true, type: Boolean })
|
||||||
|
public controls = false;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
set thumbnail_size(size: number) {
|
||||||
|
this.style.setProperty('--frigate-card-thumbnail-size', String(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// Data-binding based interface
|
||||||
|
// ============================
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected view?: Readonly<View>;
|
protected view?: Readonly<View>;
|
||||||
|
|
||||||
@@ -24,34 +38,54 @@ export class FrigateCardThumbnail extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public childIndex?: number;
|
public childIndex?: number;
|
||||||
|
|
||||||
@property({ attribute: true, type: Boolean, reflect: true })
|
// =============================================================
|
||||||
public details = false;
|
// Overrides that can be used if data bindings are not available
|
||||||
|
// =============================================================
|
||||||
|
@property({ attribute: true })
|
||||||
|
public thumbnail?: string;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: true })
|
||||||
set thumbnail_size(size: number) {
|
public label?: string;
|
||||||
this.style.setProperty('--frigate-card-thumbnail-size', String(size));
|
|
||||||
}
|
@property({ attribute: true })
|
||||||
|
public event?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the element.
|
* Render the element.
|
||||||
* @returns A template to display to the user.
|
* @returns A template to display to the user.
|
||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.target || !this.target.children || this.childIndex === undefined) {
|
let event: FrigateEvent | null = null;
|
||||||
return;
|
let thumbnail: string | null = null;
|
||||||
}
|
let label: string | null = null;
|
||||||
|
|
||||||
|
// Take the event / thumbnail / label from the data-bound media (if specified).
|
||||||
|
if (this.target && this.target.children && this.childIndex !== undefined) {
|
||||||
const media = this.target.children[this.childIndex];
|
const media = this.target.children[this.childIndex];
|
||||||
if (!media.thumbnail) {
|
event = media.frigate?.event ?? null;
|
||||||
|
thumbnail = media.thumbnail;
|
||||||
|
label = media.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always give the overrides preference (if specified).
|
||||||
|
if (this.event) {
|
||||||
|
event = JSON.parse(this.event);
|
||||||
|
}
|
||||||
|
thumbnail = this.thumbnail ? this.thumbnail : thumbnail;
|
||||||
|
label = this.label ? this.label : label;
|
||||||
|
|
||||||
|
if (!thumbnail) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = media.frigate?.event;
|
console.info;
|
||||||
|
|
||||||
return html` <img
|
return html` <img
|
||||||
aria-label="${media.title}"
|
aria-label="${label ?? ''}"
|
||||||
src="${media.thumbnail}"
|
src="${thumbnail}"
|
||||||
title="${media.title}"
|
title="${label ?? ''}"
|
||||||
/>
|
/>
|
||||||
${event?.retain_indefinitely
|
${this.controls && event?.retain_indefinitely
|
||||||
? html` <ha-icon
|
? html` <ha-icon
|
||||||
class="favorite"
|
class="favorite"
|
||||||
icon="mdi:star"
|
icon="mdi:star"
|
||||||
@@ -80,7 +114,8 @@ export class FrigateCardThumbnail extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
: html``}
|
: html``}
|
||||||
<ha-icon
|
${this.controls
|
||||||
|
? html`<ha-icon
|
||||||
class="timeline"
|
class="timeline"
|
||||||
icon="mdi:target"
|
icon="mdi:target"
|
||||||
title=${localize('thumbnail.timeline')}
|
title=${localize('thumbnail.timeline')}
|
||||||
@@ -95,7 +130,8 @@ export class FrigateCardThumbnail extends LitElement {
|
|||||||
})
|
})
|
||||||
.dispatchChangeEvent(this);
|
.dispatchChangeEvent(this);
|
||||||
}}
|
}}
|
||||||
></ha-icon>`;
|
></ha-icon>`
|
||||||
|
: ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// TODO: Hover over an event should show something useful.
|
// TODO: Hover over an event should show something useful.
|
||||||
// TODO: Periodically refetch events.
|
// TODO: Periodically refetch events.
|
||||||
|
// TODO: Editor support for timeline (incl. in views).
|
||||||
|
// TODO: Make thumbnail controls optional (in all places that use thumbnails).
|
||||||
// TODO: Search for TODOs and logging statements.
|
// TODO: Search for TODOs and logging statements.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -74,11 +76,14 @@ class TimelineEventManager {
|
|||||||
// The latest date managed.
|
// The latest date managed.
|
||||||
protected _dateEnd?: Date;
|
protected _dateEnd?: Date;
|
||||||
protected _contentCallback?: (source: FrigateBrowseMediaSource) => string;
|
protected _contentCallback?: (source: FrigateBrowseMediaSource) => string;
|
||||||
|
protected _tooltipCallback?: (source: FrigateBrowseMediaSource) => string;
|
||||||
|
|
||||||
constructor(params?: {
|
constructor(params?: {
|
||||||
contentCallback?: (source: FrigateBrowseMediaSource) => string;
|
contentCallback?: (source: FrigateBrowseMediaSource) => string;
|
||||||
|
tooltipCallback?: (source: FrigateBrowseMediaSource) => string;
|
||||||
}) {
|
}) {
|
||||||
this._contentCallback = params?.contentCallback;
|
this._contentCallback = params?.contentCallback;
|
||||||
|
this._tooltipCallback = params?.tooltipCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,6 +124,7 @@ class TimelineEventManager {
|
|||||||
id: event.id,
|
id: event.id,
|
||||||
group: camera,
|
group: camera,
|
||||||
content: this._contentCallback?.(child) ?? '',
|
content: this._contentCallback?.(child) ?? '',
|
||||||
|
title: this._tooltipCallback?.(child) ?? '',
|
||||||
start: event.start_time * 1000,
|
start: event.start_time * 1000,
|
||||||
event: event,
|
event: event,
|
||||||
};
|
};
|
||||||
@@ -306,11 +312,41 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected timelineConfig?: TimelineConfig;
|
protected timelineConfig?: TimelineConfig;
|
||||||
|
|
||||||
protected _events = new TimelineEventManager();
|
protected _events = new TimelineEventManager({
|
||||||
|
tooltipCallback: this._getTooltip.bind(this),
|
||||||
|
});
|
||||||
protected _refTimeline: Ref<HTMLElement> = createRef();
|
protected _refTimeline: Ref<HTMLElement> = createRef();
|
||||||
protected _thumbnails?: FrigateBrowseMediaSource;
|
protected _thumbnails?: FrigateBrowseMediaSource;
|
||||||
protected _timeline?: Timeline;
|
protected _timeline?: Timeline;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a tooltip for a given timeline event.
|
||||||
|
* @param source The FrigateBrowseMediaSource in question.
|
||||||
|
* @returns The tooltip as a string to render.
|
||||||
|
*/
|
||||||
|
protected _getTooltip(source: FrigateBrowseMediaSource): string {
|
||||||
|
const thumbnailSizeAttr = this.timelineConfig
|
||||||
|
? `thumbnail_size="${this.timelineConfig.controls.thumbnails.size}"`
|
||||||
|
: '';
|
||||||
|
const eventAttr = source.frigate?.event
|
||||||
|
? `event='${JSON.stringify(source.frigate.event)}'`
|
||||||
|
: '';
|
||||||
|
console.info(eventAttr);
|
||||||
|
|
||||||
|
// Cannot use Lit data-bindings as visjs requires a string for tooltips.
|
||||||
|
// Note that changes to attributes here must be mirrored in the xss
|
||||||
|
// whitelist in `_getOptions()` .
|
||||||
|
return `
|
||||||
|
<frigate-card-thumbnail
|
||||||
|
details
|
||||||
|
thumbnail="${source.thumbnail}"
|
||||||
|
label="${source.title}"
|
||||||
|
${eventAttr}
|
||||||
|
${thumbnailSizeAttr}
|
||||||
|
>
|
||||||
|
</frigate-card-thumbnail>`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master render method.
|
* Master render method.
|
||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
@@ -575,14 +611,19 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
start: start,
|
start: start,
|
||||||
end: end,
|
end: end,
|
||||||
groupHeightMode: 'fixed',
|
groupHeightMode: 'fixed',
|
||||||
|
tooltip: {
|
||||||
|
followMouse: true,
|
||||||
|
overflowMethod: 'cap',
|
||||||
|
},
|
||||||
xss: {
|
xss: {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
whiteList: {
|
whiteList: {
|
||||||
'frigate-card-timeline-event': [
|
'frigate-card-thumbnail': [
|
||||||
|
'details',
|
||||||
'thumbnail',
|
'thumbnail',
|
||||||
'label',
|
'label',
|
||||||
'media_id',
|
'event',
|
||||||
'thumbnail_size',
|
'thumbnail_size',
|
||||||
],
|
],
|
||||||
div: ['title'],
|
div: ['title'],
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
border: 1px solid var(--primary-color);
|
border: 1px solid var(--primary-color);
|
||||||
border-radius: var(--ha-card-border-radius, 4px);
|
border-radius: var(--ha-card-border-radius, 4px);
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
|
||||||
|
// When details are enabled, use a background color so that the details have
|
||||||
|
// contrast with the background.
|
||||||
|
background-color: var(--primary-background-color, black);
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
|||||||
@@ -85,4 +85,7 @@ div.vis-tooltip {
|
|||||||
padding: 0px;
|
padding: 0px;
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
|
// Use browser default font-family for tooltips.
|
||||||
|
font-family: unset;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user