From d2a7e877c369d28f863478fef77f46a6d16d07c4 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sun, 20 Mar 2022 22:47:23 -0700 Subject: [PATCH] Allow thumbnails above/below on timeline. --- src/components/thumbnail-carousel.ts | 16 ++-- src/components/timeline.ts | 113 +++++++++++++-------------- src/scss/thumbnail.scss | 2 + src/scss/timeline-event.scss | 16 ---- src/scss/timeline.scss | 4 +- src/types.ts | 45 +++++------ 6 files changed, 91 insertions(+), 105 deletions(-) delete mode 100644 src/scss/timeline-event.scss diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts index fa3b3e52..e0ab4225 100644 --- a/src/components/thumbnail-carousel.ts +++ b/src/components/thumbnail-carousel.ts @@ -2,7 +2,7 @@ import { BrowseMediaUtil } from '../browse-media-util.js'; import { CSSResultGroup, TemplateResult, html, unsafeCSS, PropertyValues } from 'lit'; import { EmblaOptionsType } from 'embla-carousel'; import { classMap } from 'lit/directives/class-map.js'; -import { customElement, property } from 'lit/decorators.js'; +import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import type { FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js'; @@ -32,7 +32,13 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { public selected?: number | null; @property({ attribute: false }) - public config?: ThumbnailsControlConfig; + set config(config: ThumbnailsControlConfig) { + this.direction = ['left', 'right'].includes(config.mode) ? 'vertical' : 'horizontal'; + this._config = config; + } + + @state() + protected _config?: ThumbnailsControlConfig; @property({ attribute: false }) set highlight_selected(value: boolean) { @@ -120,8 +126,8 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { return html` { if (this._carousel && this._carousel.clickAllowed()) { @@ -143,7 +149,7 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { */ protected render(): TemplateResult | void { const slides = this._getSlides(); - if (!slides || !this.config || this.config.mode == 'none') { + if (!slides || !this._config || this._config.mode == 'none') { return; } diff --git a/src/components/timeline.ts b/src/components/timeline.ts index ab69726e..86d79910 100644 --- a/src/components/timeline.ts +++ b/src/components/timeline.ts @@ -174,52 +174,64 @@ export class FrigateCardTimeline extends LitElement { protected _thumbnailsRef: Ref = createRef(); protected _timeline?: Timeline; - protected _events = new TimelineEventManager() + protected _events = new TimelineEventManager(); /** * Master render method. * @returns A rendered template. */ protected render(): TemplateResult | void { - if (!this.hass || !this.view) { + if (!this.hass || !this.view || !this._timelineConfig) { return; } - const config: ThumbnailsControlConfig = { - mode: 'above', - show_details: true, - }; + const thumbnailsConfig = this._timelineConfig.controls.thumbnails; - // TODO move to configuration later. - const drawerLocation: "left" | "right" = "left" as const; + const drawer = ['left', 'right'].includes(thumbnailsConfig.mode) + ? (thumbnailsConfig.mode as 'left' | 'right') + : null; const timelineClasses = { - "timeline": true, - "left-margin": drawerLocation == "left", - //"right-margin": drawerLocation == "right", - } + timeline: true, + 'left-margin': drawer === 'left', + 'right-margin': drawer === 'right', + }; - return html` - ) => { - if (ev.detail.target && ev.detail.childIndex) { - this.view - ?.evolve({ - target: ev.detail.target, - childIndex: ev.detail.childIndex, - view: 'clip', - }) - .dispatchChangeEvent(this); - } - }} - > - - -
`; + const renderThumbnails = (): TemplateResult => { + const renderCarousel = (): TemplateResult => { + return html` + ) => { + if (ev.detail.target && ev.detail.childIndex) { + this.view + ?.evolve({ + target: ev.detail.target, + childIndex: ev.detail.childIndex, + view: 'clip', + }) + .dispatchChangeEvent(this); + } + }} + > + + `; + }; + + return drawer + ? html` + ${renderCarousel()} + ` + : renderCarousel(); + }; + + return html` + ${thumbnailsConfig.mode === 'above' ? renderThumbnails() : ''} +
+ ${thumbnailsConfig.mode !== 'above' ? renderThumbnails() : ''} + `; } /** @@ -252,15 +264,11 @@ export class FrigateCardTimeline extends LitElement { // 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, - ).then(() => { - this._updateThumbnails(); - }) + this._events + .fetchEvents(this, this.hass, this.cameras, properties.start, properties.end) + .then(() => { + this._updateThumbnails(); + }); } } @@ -329,11 +337,9 @@ export class FrigateCardTimeline extends LitElement { children: children, }; - if (this._drawerRef.value) { - if (this._thumbnailsRef.value) { - this._thumbnailsRef.value.target = target; - this._thumbnailsRef.value.selected = childIndex ?? undefined; - } + if (this._thumbnailsRef.value) { + this._thumbnailsRef.value.target = target; + this._thumbnailsRef.value.selected = childIndex ?? undefined; } } @@ -360,17 +366,12 @@ export class FrigateCardTimeline extends LitElement { return; } - const thumbnailConfig = - this._timelineConfig?.controls.thumbnails ?? - frigateCardConfigDefaults.timeline.controls.thumbnails; - // Configuration for the Timeline, see: // https://visjs.github.io/vis-timeline/docs/timeline/#Configuration_Options this._timelineOptions = { cluster: - thumbnailConfig.clustering_threshold > 0 + this._timelineConfig.clustering_threshold > 0 ? { - fitOnDoubleClick: true, showStipes: true, // It would be better to automatically calculate `maxItems` from the // rendered height of the timeline (or group within the timeline) so @@ -380,15 +381,11 @@ export class FrigateCardTimeline extends LitElement { // 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, + maxItems: this._timelineConfig.clustering_threshold, } : (false as TimelineOptionsCluster), minHeight: '100%', maxHeight: '100%', - tooltip: { - followMouse: true, - overflowMethod: 'cap', - }, zoomMax: 31 * 24 * 60 * 60 * 1000, zoomMin: 1 * 1000, selectable: true, diff --git a/src/scss/thumbnail.scss b/src/scss/thumbnail.scss index 14795b33..d48e60e8 100644 --- a/src/scss/thumbnail.scss +++ b/src/scss/thumbnail.scss @@ -29,6 +29,8 @@ img { // Restrict images to a maximum of thumbnail size. max-width: var(--frigate-card-thumbnail-size); max-height: var(--frigate-card-thumbnail-size); + min-width: var(--frigate-card-thumbnail-size); + min-height: var(--frigate-card-thumbnail-size); transition: transform 0.2s linear; } diff --git a/src/scss/timeline-event.scss b/src/scss/timeline-event.scss deleted file mode 100644 index 18294c49..00000000 --- a/src/scss/timeline-event.scss +++ /dev/null @@ -1,16 +0,0 @@ -:host { - display: block; - width: 100%; - height: 100%; - - --frigate-card-timeline-thumbnail-size: 75px; - - border-radius: 5px; - overflow: hidden; -} - -img { - width: var(--frigate-card-timeline-thumbnail-size); - height: var(--frigate-card-timeline-thumbnail-size); - display: block; -} diff --git a/src/scss/timeline.scss b/src/scss/timeline.scss index fe15c743..7a65f9ec 100644 --- a/src/scss/timeline.scss +++ b/src/scss/timeline.scss @@ -17,11 +17,11 @@ div.timeline { } div.timeline.left-margin { // Clearance for the drawer button. - margin-left: drawer.$drawer-icon-size; + margin-left: calc(drawer.$drawer-icon-size + 1px); } div.timeline.right-margin { // Clearance for the drawer button. - margin-right: drawer.$drawer-icon-size; + margin-right: calc(drawer.$drawer-icon-size + 1px); } .vis-text { diff --git a/src/types.ts b/src/types.ts index 34e660e1..3cd65d15 100644 --- a/src/types.ts +++ b/src/types.ts @@ -434,7 +434,7 @@ export type ImageViewConfig = z.infer; */ const thumbnailsControlSchema = z.object({ - mode: z.enum(['none', 'above', 'below']), + mode: z.enum(['none', 'above', 'below', 'left', 'right']), size: z.string().optional(), show_details: z.boolean().optional(), }); @@ -660,7 +660,6 @@ const viewerNextPreviousControlConfigSchema = nextPreviousControlConfigSchema.ex .enum(['none', 'thumbnails', 'chevrons']) .default(viewerConfigDefault.controls.next_previous.style), size: z.string().default(viewerConfigDefault.controls.next_previous.size), - }); export type ViewerNextPreviousControlConfig = z.infer< typeof viewerNextPreviousControlConfigSchema @@ -755,33 +754,31 @@ const dimensionsConfigSchema = z * Timeline configuration section. */ const timelineConfigDefault = { + clustering_threshold: 3, controls: { thumbnails: { - size_pixels: 75, - overlap_pixels: 25, - clustering_threshold: 3, + mode: 'left' as const, + size: '100px' as const, + show_details: true, }, }, }; const timelineConfigSchema = z .object({ + clustering_threshold: z.number().default(timelineConfigDefault.clustering_threshold), 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), + thumbnails: thumbnailsControlSchema + .extend({ + mode: thumbnailsControlSchema.shape.mode.default( + timelineConfigDefault.controls.thumbnails.mode, + ), + size: thumbnailsControlSchema.shape.size.default( + timelineConfigDefault.controls.thumbnails.size, + ), + show_details: thumbnailsControlSchema.shape.show_details.default( + timelineConfigDefault.controls.thumbnails.show_details, + ), }) .default(timelineConfigDefault.controls.thumbnails), }) @@ -922,9 +919,9 @@ export interface FrigateCardMediaPlayer { * Home Assistant API types. */ -export const MEDIA_CLASS_PLAYLIST = "playlist" as const; -export const MEDIA_CLASS_VIDEO = "video" as const; -export const MEDIA_TYPE_VIDEO = "video" as const; +export const MEDIA_CLASS_PLAYLIST = 'playlist' as const; +export const MEDIA_CLASS_VIDEO = 'video' as const; +export const MEDIA_TYPE_VIDEO = 'video' as const; // Recursive type, cannot use type interference: // See: https://github.com/colinhacks/zod#recursive-types @@ -959,7 +956,7 @@ export interface FrigateEvent { export interface FrigateBrowseMediaSource extends BrowseMediaSource { children?: FrigateBrowseMediaSource[] | null; frigate?: { - event: FrigateEvent, + event: FrigateEvent; }; }