Allow thumbnails above/below on timeline.
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+55
-58
@@ -174,52 +174,64 @@ 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 => {
|
||||||
<frigate-card-thumbnail-carousel
|
const renderCarousel = (): TemplateResult => {
|
||||||
${ref(this._thumbnailsRef)}
|
return html`
|
||||||
direction="vertical"
|
<frigate-card-thumbnail-carousel
|
||||||
.config=${config}
|
${ref(this._thumbnailsRef)}
|
||||||
.highlight_selected=${true}
|
.config=${thumbnailsConfig}
|
||||||
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
|
.highlight_selected=${true}
|
||||||
if (ev.detail.target && ev.detail.childIndex) {
|
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
|
||||||
this.view
|
if (ev.detail.target && ev.detail.childIndex) {
|
||||||
?.evolve({
|
this.view
|
||||||
target: ev.detail.target,
|
?.evolve({
|
||||||
childIndex: ev.detail.childIndex,
|
target: ev.detail.target,
|
||||||
view: 'clip',
|
childIndex: ev.detail.childIndex,
|
||||||
})
|
view: 'clip',
|
||||||
.dispatchChangeEvent(this);
|
})
|
||||||
}
|
.dispatchChangeEvent(this);
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
</frigate-card-thumbnail-carousel>
|
>
|
||||||
</frigate-card-drawer>
|
</frigate-card-thumbnail-carousel>
|
||||||
<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,
|
this._updateThumbnails();
|
||||||
properties.start,
|
});
|
||||||
properties.end,
|
|
||||||
).then(() => {
|
|
||||||
this._updateThumbnails();
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,11 +337,9 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user