Show thumbnails as tooltips.

This commit is contained in:
Dermot Duffy
2022-04-09 09:39:34 -07:00
parent 79bf66d961
commit 616c24c94b
5 changed files with 120 additions and 35 deletions
+1
View File
@@ -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
View File
@@ -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>`
: ''}`;
} }
/** /**
+44 -3
View File
@@ -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'],
+4
View File
@@ -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 {
+3
View File
@@ -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;
} }