Allow thumbnails above/below on timeline.

This commit is contained in:
Dermot Duffy
2022-03-20 22:47:23 -07:00
parent b04d6c11fc
commit d2a7e877c3
6 changed files with 91 additions and 105 deletions
+11 -5
View File
@@ -2,7 +2,7 @@ import { BrowseMediaUtil } from '../browse-media-util.js';
import { CSSResultGroup, TemplateResult, html, unsafeCSS, PropertyValues } from 'lit'; import { CSSResultGroup, TemplateResult, html, unsafeCSS, PropertyValues } from 'lit';
import { EmblaOptionsType } from 'embla-carousel'; import { EmblaOptionsType } from 'embla-carousel';
import { classMap } from 'lit/directives/class-map.js'; 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 { ifDefined } from 'lit/directives/if-defined.js';
import type { FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js'; import type { FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js';
@@ -32,7 +32,13 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
public selected?: number | null; public selected?: number | null;
@property({ attribute: false }) @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 }) @property({ attribute: false })
set highlight_selected(value: boolean) { set highlight_selected(value: boolean) {
@@ -120,8 +126,8 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
return html` return html`
<frigate-card-thumbnail <frigate-card-thumbnail
.media=${mediaToRender} .media=${mediaToRender}
?details=${this.config?.show_details} ?details=${this._config?.show_details}
thumbnail_size=${ifDefined(this.config?.size)} thumbnail_size=${ifDefined(this._config?.size)}
class="${classMap(classes)}" class="${classMap(classes)}"
@click=${(ev) => { @click=${(ev) => {
if (this._carousel && this._carousel.clickAllowed()) { if (this._carousel && this._carousel.clickAllowed()) {
@@ -143,7 +149,7 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
*/ */
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
const slides = this._getSlides(); const slides = this._getSlides();
if (!slides || !this.config || this.config.mode == 'none') { if (!slides || !this._config || this._config.mode == 'none') {
return; return;
} }
+35 -38
View File
@@ -174,36 +174,35 @@ export class FrigateCardTimeline extends LitElement {
protected _thumbnailsRef: Ref<FrigateCardThumbnailCarousel> = createRef(); protected _thumbnailsRef: Ref<FrigateCardThumbnailCarousel> = createRef();
protected _timeline?: Timeline; protected _timeline?: Timeline;
protected _events = new TimelineEventManager() protected _events = new TimelineEventManager();
/** /**
* Master render method. * Master render method.
* @returns A rendered template. * @returns A rendered template.
*/ */
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
if (!this.hass || !this.view) { if (!this.hass || !this.view || !this._timelineConfig) {
return; return;
} }
const config: ThumbnailsControlConfig = { const thumbnailsConfig = this._timelineConfig.controls.thumbnails;
mode: 'above',
show_details: true,
};
// TODO move to configuration later. const drawer = ['left', 'right'].includes(thumbnailsConfig.mode)
const drawerLocation: "left" | "right" = "left" as const; ? (thumbnailsConfig.mode as 'left' | 'right')
: null;
const timelineClasses = { const timelineClasses = {
"timeline": true, timeline: true,
"left-margin": drawerLocation == "left", 'left-margin': drawer === 'left',
//"right-margin": drawerLocation == "right", 'right-margin': drawer === 'right',
} };
return html` <frigate-card-drawer location="${drawerLocation}" ${ref(this._drawerRef)}> const renderThumbnails = (): TemplateResult => {
const renderCarousel = (): TemplateResult => {
return html`
<frigate-card-thumbnail-carousel <frigate-card-thumbnail-carousel
${ref(this._thumbnailsRef)} ${ref(this._thumbnailsRef)}
direction="vertical" .config=${thumbnailsConfig}
.config=${config}
.highlight_selected=${true} .highlight_selected=${true}
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => { @frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
if (ev.detail.target && ev.detail.childIndex) { if (ev.detail.target && ev.detail.childIndex) {
@@ -218,8 +217,21 @@ export class FrigateCardTimeline extends LitElement {
}} }}
> >
</frigate-card-thumbnail-carousel> </frigate-card-thumbnail-carousel>
</frigate-card-drawer> `;
<div class="${classMap(timelineClasses)}" ${ref(this._timelineRef)}></div>`; };
return drawer
? html`<frigate-card-drawer location="${drawer}" ${ref(this._drawerRef)}>
${renderCarousel()}
</frigate-card-drawer>`
: renderCarousel();
};
return html`
${thumbnailsConfig.mode === 'above' ? renderThumbnails() : ''}
<div class="${classMap(timelineClasses)}" ${ref(this._timelineRef)}></div>
${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 // 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 // not trivial to implement, and it's not yet clear it's worth the extra
// complexity. // complexity.
this._events.fetchEvents( this._events
this, .fetchEvents(this, this.hass, this.cameras, properties.start, properties.end)
this.hass, .then(() => {
this.cameras,
properties.start,
properties.end,
).then(() => {
this._updateThumbnails(); this._updateThumbnails();
}) });
} }
} }
@@ -329,13 +337,11 @@ export class FrigateCardTimeline extends LitElement {
children: children, children: children,
}; };
if (this._drawerRef.value) {
if (this._thumbnailsRef.value) { if (this._thumbnailsRef.value) {
this._thumbnailsRef.value.target = target; this._thumbnailsRef.value.target = target;
this._thumbnailsRef.value.selected = childIndex ?? undefined; this._thumbnailsRef.value.selected = childIndex ?? undefined;
} }
} }
}
/** /**
* Build the visjs dataset to render on the timeline. * Build the visjs dataset to render on the timeline.
@@ -360,17 +366,12 @@ export class FrigateCardTimeline extends LitElement {
return; return;
} }
const thumbnailConfig =
this._timelineConfig?.controls.thumbnails ??
frigateCardConfigDefaults.timeline.controls.thumbnails;
// 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: cluster:
thumbnailConfig.clustering_threshold > 0 this._timelineConfig.clustering_threshold > 0
? { ? {
fitOnDoubleClick: true,
showStipes: true, showStipes: true,
// It would be better to automatically calculate `maxItems` from the // It would be better to automatically calculate `maxItems` from the
// rendered height of the timeline (or group within the timeline) so // 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 // and if we adjust `maxItems` then we can get into an infinite
// resize loop. Adjusting the `maxItems` of a timeline, after it's // resize loop. Adjusting the `maxItems` of a timeline, after it's
// created, also does not appear to work as expected. // created, also does not appear to work as expected.
maxItems: thumbnailConfig.clustering_threshold, maxItems: this._timelineConfig.clustering_threshold,
} }
: (false as TimelineOptionsCluster), : (false as TimelineOptionsCluster),
minHeight: '100%', minHeight: '100%',
maxHeight: '100%', maxHeight: '100%',
tooltip: {
followMouse: true,
overflowMethod: 'cap',
},
zoomMax: 31 * 24 * 60 * 60 * 1000, zoomMax: 31 * 24 * 60 * 60 * 1000,
zoomMin: 1 * 1000, zoomMin: 1 * 1000,
selectable: true, selectable: true,
+2
View File
@@ -29,6 +29,8 @@ img {
// Restrict images to a maximum of thumbnail size. // Restrict images to a maximum of thumbnail size.
max-width: var(--frigate-card-thumbnail-size); max-width: var(--frigate-card-thumbnail-size);
max-height: 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; transition: transform 0.2s linear;
} }
-16
View File
@@ -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;
}
+2 -2
View File
@@ -17,11 +17,11 @@ div.timeline {
} }
div.timeline.left-margin { div.timeline.left-margin {
// Clearance for the drawer button. // Clearance for the drawer button.
margin-left: drawer.$drawer-icon-size; margin-left: calc(drawer.$drawer-icon-size + 1px);
} }
div.timeline.right-margin { div.timeline.right-margin {
// Clearance for the drawer button. // Clearance for the drawer button.
margin-right: drawer.$drawer-icon-size; margin-right: calc(drawer.$drawer-icon-size + 1px);
} }
.vis-text { .vis-text {
+21 -24
View File
@@ -434,7 +434,7 @@ export type ImageViewConfig = z.infer<typeof imageConfigSchema>;
*/ */
const thumbnailsControlSchema = z.object({ const thumbnailsControlSchema = z.object({
mode: z.enum(['none', 'above', 'below']), mode: z.enum(['none', 'above', 'below', 'left', 'right']),
size: z.string().optional(), size: z.string().optional(),
show_details: z.boolean().optional(), show_details: z.boolean().optional(),
}); });
@@ -660,7 +660,6 @@ const viewerNextPreviousControlConfigSchema = nextPreviousControlConfigSchema.ex
.enum(['none', 'thumbnails', 'chevrons']) .enum(['none', 'thumbnails', 'chevrons'])
.default(viewerConfigDefault.controls.next_previous.style), .default(viewerConfigDefault.controls.next_previous.style),
size: z.string().default(viewerConfigDefault.controls.next_previous.size), size: z.string().default(viewerConfigDefault.controls.next_previous.size),
}); });
export type ViewerNextPreviousControlConfig = z.infer< export type ViewerNextPreviousControlConfig = z.infer<
typeof viewerNextPreviousControlConfigSchema typeof viewerNextPreviousControlConfigSchema
@@ -755,33 +754,31 @@ const dimensionsConfigSchema = z
* Timeline configuration section. * Timeline configuration section.
*/ */
const timelineConfigDefault = { const timelineConfigDefault = {
clustering_threshold: 3,
controls: { controls: {
thumbnails: { thumbnails: {
size_pixels: 75, mode: 'left' as const,
overlap_pixels: 25, size: '100px' as const,
clustering_threshold: 3, show_details: true,
}, },
}, },
}; };
const timelineConfigSchema = z const timelineConfigSchema = z
.object({ .object({
clustering_threshold: z.number().default(timelineConfigDefault.clustering_threshold),
controls: z controls: z
.object({ .object({
thumbnails: z thumbnails: thumbnailsControlSchema
.object({ .extend({
size_pixels: z mode: thumbnailsControlSchema.shape.mode.default(
.number() timelineConfigDefault.controls.thumbnails.mode,
.min(50) ),
.max(THUMBNAIL_WIDTH_MAX) size: thumbnailsControlSchema.shape.size.default(
.default(timelineConfigDefault.controls.thumbnails.size_pixels), timelineConfigDefault.controls.thumbnails.size,
overlap_pixels: z ),
.number() show_details: thumbnailsControlSchema.shape.show_details.default(
.min(0) timelineConfigDefault.controls.thumbnails.show_details,
.max(THUMBNAIL_WIDTH_MAX) ),
.default(timelineConfigDefault.controls.thumbnails.overlap_pixels),
clustering_threshold: z
.number()
.default(timelineConfigDefault.controls.thumbnails.clustering_threshold),
}) })
.default(timelineConfigDefault.controls.thumbnails), .default(timelineConfigDefault.controls.thumbnails),
}) })
@@ -922,9 +919,9 @@ export interface FrigateCardMediaPlayer {
* Home Assistant API types. * Home Assistant API types.
*/ */
export const MEDIA_CLASS_PLAYLIST = "playlist" as const; export const MEDIA_CLASS_PLAYLIST = 'playlist' as const;
export const MEDIA_CLASS_VIDEO = "video" as const; export const MEDIA_CLASS_VIDEO = 'video' as const;
export const MEDIA_TYPE_VIDEO = "video" as const; export const MEDIA_TYPE_VIDEO = 'video' as const;
// Recursive type, cannot use type interference: // Recursive type, cannot use type interference:
// See: https://github.com/colinhacks/zod#recursive-types // See: https://github.com/colinhacks/zod#recursive-types
@@ -959,7 +956,7 @@ export interface FrigateEvent {
export interface FrigateBrowseMediaSource extends BrowseMediaSource { export interface FrigateBrowseMediaSource extends BrowseMediaSource {
children?: FrigateBrowseMediaSource[] | null; children?: FrigateBrowseMediaSource[] | null;
frigate?: { frigate?: {
event: FrigateEvent, event: FrigateEvent;
}; };
} }