From 616c24c94b23127c899f700daf09038fb4d7c589 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 9 Apr 2022 09:39:34 -0700 Subject: [PATCH] Show thumbnails as tooltips. --- src/components/thumbnail-carousel.ts | 1 + src/components/thumbnail.ts | 100 ++++++++++++++++++--------- src/components/timeline.ts | 47 ++++++++++++- src/scss/thumbnail.scss | 4 ++ src/scss/timeline-core.scss | 3 + 5 files changed, 120 insertions(+), 35 deletions(-) diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts index a0b0b449..9d2f01c1 100644 --- a/src/components/thumbnail-carousel.ts +++ b/src/components/thumbnail-carousel.ts @@ -179,6 +179,7 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { .target=${parent} .childIndex=${childIndex} ?details=${this._config?.show_details} + ?controls=${true} thumbnail_size=${ifDefined(this._config?.size)} class="${classMap(classes)}" @click=${(ev) => { diff --git a/src/components/thumbnail.ts b/src/components/thumbnail.ts index 0ba65552..c4b14dab 100644 --- a/src/components/thumbnail.ts +++ b/src/components/thumbnail.ts @@ -2,7 +2,7 @@ import { CSSResult, TemplateResult, html, unsafeCSS, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; 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 { getEventDurationString, @@ -15,6 +15,20 @@ import thumbnailStyle from '../scss/thumbnail.scss'; @customElement('frigate-card-thumbnail') 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 }) protected view?: Readonly; @@ -24,34 +38,54 @@ export class FrigateCardThumbnail extends LitElement { @property({ attribute: false }) 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 }) - set thumbnail_size(size: number) { - this.style.setProperty('--frigate-card-thumbnail-size', String(size)); - } + @property({ attribute: true }) + public label?: string; + + @property({ attribute: true }) + public event?: string; /** * Render the element. * @returns A template to display to the user. */ protected render(): TemplateResult | void { - if (!this.target || !this.target.children || this.childIndex === undefined) { - return; + let event: FrigateEvent | null = null; + 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]; + event = media.frigate?.event ?? null; + thumbnail = media.thumbnail; + label = media.title; } - const media = this.target.children[this.childIndex]; - if (!media.thumbnail) { + + // 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; } - const event = media.frigate?.event; + console.info; + return html` - ${event?.retain_indefinitely + ${this.controls && event?.retain_indefinitely ? html` ` : html``} - { - stopEventFromActivatingCardWideActions(ev); - this.view - ?.evolve({ - view: 'timeline', - target: this.target, - childIndex: this.childIndex ?? null, - context: {}, - }) - .dispatchChangeEvent(this); - }} - >`; + ${this.controls + ? html` { + stopEventFromActivatingCardWideActions(ev); + this.view + ?.evolve({ + view: 'timeline', + target: this.target, + childIndex: this.childIndex ?? null, + context: {}, + }) + .dispatchChangeEvent(this); + }} + >` + : ''}`; } /** diff --git a/src/components/timeline.ts b/src/components/timeline.ts index dba50bf1..f25471b6 100644 --- a/src/components/timeline.ts +++ b/src/components/timeline.ts @@ -1,5 +1,7 @@ // TODO: Hover over an event should show something useful. // 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. import { @@ -74,11 +76,14 @@ class TimelineEventManager { // The latest date managed. protected _dateEnd?: Date; protected _contentCallback?: (source: FrigateBrowseMediaSource) => string; + protected _tooltipCallback?: (source: FrigateBrowseMediaSource) => string; constructor(params?: { contentCallback?: (source: FrigateBrowseMediaSource) => string; + tooltipCallback?: (source: FrigateBrowseMediaSource) => string; }) { this._contentCallback = params?.contentCallback; + this._tooltipCallback = params?.tooltipCallback; } /** @@ -119,6 +124,7 @@ class TimelineEventManager { id: event.id, group: camera, content: this._contentCallback?.(child) ?? '', + title: this._tooltipCallback?.(child) ?? '', start: event.start_time * 1000, event: event, }; @@ -306,11 +312,41 @@ export class FrigateCardTimelineCore extends LitElement { @property({ attribute: false }) protected timelineConfig?: TimelineConfig; - protected _events = new TimelineEventManager(); + protected _events = new TimelineEventManager({ + tooltipCallback: this._getTooltip.bind(this), + }); protected _refTimeline: Ref = createRef(); protected _thumbnails?: FrigateBrowseMediaSource; 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 ` + + `; + } + /** * Master render method. * @returns A rendered template. @@ -575,14 +611,19 @@ export class FrigateCardTimelineCore extends LitElement { start: start, end: end, groupHeightMode: 'fixed', + tooltip: { + followMouse: true, + overflowMethod: 'cap', + }, xss: { disabled: false, filterOptions: { whiteList: { - 'frigate-card-timeline-event': [ + 'frigate-card-thumbnail': [ + 'details', 'thumbnail', 'label', - 'media_id', + 'event', 'thumbnail_size', ], div: ['title'], diff --git a/src/scss/thumbnail.scss b/src/scss/thumbnail.scss index d48e60e8..2aa3ec48 100644 --- a/src/scss/thumbnail.scss +++ b/src/scss/thumbnail.scss @@ -16,6 +16,10 @@ border: 1px solid var(--primary-color); border-radius: var(--ha-card-border-radius, 4px); 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 { diff --git a/src/scss/timeline-core.scss b/src/scss/timeline-core.scss index 4d90158a..479c7175 100644 --- a/src/scss/timeline-core.scss +++ b/src/scss/timeline-core.scss @@ -85,4 +85,7 @@ div.vis-tooltip { padding: 0px; background-color: unset; border: none; + + // Use browser default font-family for tooltips. + font-family: unset; }