Add generic support for a 'surround'.

This commit is contained in:
Dermot Duffy
2022-03-22 19:58:34 -07:00
parent b649401f11
commit a4fcf2513b
9 changed files with 370 additions and 179 deletions
+2 -2
View File
@@ -115,11 +115,11 @@ export async function homeAssistantSignPath(
* @param detail An optional detail object to attach. * @param detail An optional detail object to attach.
*/ */
export function dispatchFrigateCardEvent<T>( export function dispatchFrigateCardEvent<T>(
element: HTMLElement, target: EventTarget,
name: string, name: string,
detail?: T, detail?: T,
): void { ): void {
element.dispatchEvent( target.dispatchEvent(
new CustomEvent<T>(`frigate-card:${name}`, { new CustomEvent<T>(`frigate-card:${name}`, {
bubbles: true, bubbles: true,
composed: true, composed: true,
+19 -18
View File
@@ -21,23 +21,11 @@ export class FrigateCardDrawer extends LitElement {
@property({ attribute: true, reflect: true, type: Boolean }) @property({ attribute: true, reflect: true, type: Boolean })
public control = true; public control = true;
/**
* Set the timeline configuration.
*/
@property({ type: Boolean, reflect: true, attribute: true }) @property({ type: Boolean, reflect: true, attribute: true })
set open(open: boolean) { public open = false;
if (this._drawerRef.value) {
const old = this._drawerRef.value.open;
this._drawerRef.value.open = open;
this.requestUpdate('open', old);
}
}
get open(): boolean { protected _refDrawer: Ref<HTMLElement & { open: boolean }> = createRef();
return this._drawerRef.value?.open ?? false; protected _refSlot: Ref<HTMLSlotElement> = createRef();
}
protected _drawerRef: Ref<HTMLElement & { open: boolean }> = createRef();
/** /**
* Called on the first update. * Called on the first update.
@@ -51,12 +39,25 @@ export class FrigateCardDrawer extends LitElement {
// override the style to customize the drawer to be absolute within the div. // override the style to customize the drawer to be absolute within the div.
const style = document.createElement('style'); const style = document.createElement('style');
style.innerHTML = drawerInjectStyle; style.innerHTML = drawerInjectStyle;
this._drawerRef.value?.shadowRoot?.appendChild(style); this._refDrawer.value?.shadowRoot?.appendChild(style);
}
protected _slotChanged(): void {
const elements = this._refSlot.value?.assignedElements({ flatten: true });
if (elements && elements.length && this._refDrawer.value) {
// Hide the drawer unless there is content.
this._refDrawer.value.hidden = false;
}
} }
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<side-drawer location="${this.location}" ${ref(this._drawerRef)}> <side-drawer
${ref(this._refDrawer)}
?hidden=${true}
location="${this.location}"
?open=${this.open}
>
${this.control ${this.control
? html` ? html`
<div <div
@@ -73,7 +74,7 @@ export class FrigateCardDrawer extends LitElement {
</div> </div>
` `
: ''} : ''}
<slot></slot> <slot ${ref(this._refSlot)} @slotchange=${this._slotChanged}></slot>
</side-drawer> </side-drawer>
`; `;
} }
+103
View File
@@ -0,0 +1,103 @@
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import { HomeAssistant } from 'custom-card-helpers';
import { createRef, ref, Ref } from 'lit/directives/ref.js';
import { customElement, property } from 'lit/decorators.js';
import {
ExtendedHomeAssistant,
FrigateBrowseMediaSource,
FrigateCardView,
ThumbnailsControlConfig,
} from '../types.js';
import {
FrigateCardThumbnailCarousel,
ThumbnailCarouselTap,
} from './thumbnail-carousel.js';
import { View } from '../view.js';
import { dispatchFrigateCardEvent } from '../common.js';
import './surround.js';
import surroundThumbnailsStyle from '../scss/surround.scss';
interface FrigateCardThumbnailsSet {
target: FrigateBrowseMediaSource;
childIndex?: number;
}
@customElement('frigate-card-surround-thumbnails')
export class FrigateCardSurround extends LitElement {
@property({ attribute: false })
protected hass?: HomeAssistant & ExtendedHomeAssistant;
@property({ attribute: false })
protected view?: Readonly<View>;
@property({ attribute: false })
protected config?: ThumbnailsControlConfig;
@property({ attribute: false })
protected targetView?: FrigateCardView;
protected _refThumbnails: Ref<FrigateCardThumbnailCarousel> = createRef();
/**
* Master render method.
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
if (!this.hass || !this.view || !this.config) {
return;
}
return html` <frigate-card-surround
@frigate-card:thumbnails:set=${(ev: CustomEvent<FrigateCardThumbnailsSet>) => {
if (this._refThumbnails.value) {
this._refThumbnails.value.target = ev.detail.target;
this._refThumbnails.value.selected = ev.detail.childIndex ?? undefined;
}
}}
@frigate-card:thumbnails:open=${(ev: CustomEvent) => {
if (this.config && ['left', 'right'].includes(this.config.mode)) {
// Protects encapsulation: Catches the request to view thumbnails and
// re-dispatches a request to open the drawer (if the thumbnails are
// in a drawer). The new event needs to be dispatched from the origin
// of the inbound event, so it can be handled by
// <frigate-card-surround> .
dispatchFrigateCardEvent(ev.composedPath()[0], 'drawer:open', {
drawer: this.config.mode,
});
}
}}
>
${this.config?.mode !== 'none'
? html` <frigate-card-thumbnail-carousel
slot=${this.config.mode}
${ref(this._refThumbnails)}
.config=${this.config}
.highlight_selected=${true}
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
if (ev.detail.target && ev.detail.childIndex) {
this.view
?.evolve({
...(this.targetView && { view: this.targetView }),
target: ev.detail.target,
childIndex: ev.detail.childIndex,
})
.dispatchChangeEvent(this);
}
}}
>
</frigate-card-thumbnail-carousel>`
: ''}
<slot></slot>
</frigate-card-surround>`;
}
/**
* Return compiled CSS styles.
*/
static get styles(): CSSResultGroup {
return unsafeCSS(surroundThumbnailsStyle);
}
}
+68
View File
@@ -0,0 +1,68 @@
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import { createRef, ref, Ref } from 'lit/directives/ref.js';
import { customElement } from 'lit/decorators.js';
import { FrigateCardDrawer } from './drawer.js';
import './drawer.js';
import surroundStyle from '../scss/surround.scss';
interface FrigateCardDrawerOpen {
drawer: 'left' | 'right';
}
@customElement('frigate-card-surround')
export class FrigateCardSurround extends LitElement {
protected _refDrawerLeft: Ref<FrigateCardDrawer> = createRef();
protected _refDrawerRight: Ref<FrigateCardDrawer> = createRef();
protected _boundDrawerOpenHandler = this._drawerOpen.bind(this);
/**
* Component connected callback.
*/
connectedCallback(): void {
super.connectedCallback();
this.addEventListener('frigate-card:drawer:open', this._boundDrawerOpenHandler);
}
/**
* Component disconnected callback.
*/
disconnectedCallback(): void {
super.disconnectedCallback();
this.removeEventListener('frigate-card:drawer:open', this._boundDrawerOpenHandler);
}
protected _drawerOpen(ev: Event) {
const drawer = (ev as CustomEvent<FrigateCardDrawerOpen>).detail.drawer;
if (drawer === 'left' && this._refDrawerLeft.value) {
this._refDrawerLeft.value.open = true;
} else if (drawer === 'right' && this._refDrawerRight.value) {
this._refDrawerRight.value.open = true;
}
}
/**
* Master render method.
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
return html` <slot name="above"></slot>
<slot></slot>
<frigate-card-drawer ${ref(this._refDrawerLeft)} location="left">
<slot name="left"></slot>
</frigate-card-drawer>
<frigate-card-drawer ${ref(this._refDrawerRight)} location="right">
<slot name="right"></slot>
</frigate-card-drawer>
<slot name="below"></slot>`;
}
/**
* Return compiled CSS styles.
*/
static get styles(): CSSResultGroup {
return unsafeCSS(surroundStyle);
}
}
+66 -74
View File
@@ -23,28 +23,23 @@ import {
CameraConfig, CameraConfig,
ExtendedHomeAssistant, ExtendedHomeAssistant,
FrigateBrowseMediaSource, FrigateBrowseMediaSource,
ThumbnailsControlConfig,
MEDIA_CLASS_PLAYLIST, MEDIA_CLASS_PLAYLIST,
MEDIA_TYPE_VIDEO, MEDIA_TYPE_VIDEO,
MEDIA_CLASS_VIDEO, MEDIA_CLASS_VIDEO,
TimelineConfig, TimelineConfig,
frigateCardConfigDefaults,
} from '../types'; } from '../types';
import { FrigateCardDrawer } from './drawer';
import {
FrigateCardThumbnailCarousel,
ThumbnailCarouselTap,
} from './thumbnail-carousel';
import { View } from '../view'; import { View } from '../view';
import { import {
contentsChanged, contentsChanged,
dispatchErrorMessageEvent, dispatchErrorMessageEvent,
dispatchFrigateCardEvent,
getCameraTitle, getCameraTitle,
} from '../common.js'; } from '../common.js';
import timelineCoreStyle from '../scss/timeline-core.scss';
import timelineStyle from '../scss/timeline.scss'; import timelineStyle from '../scss/timeline.scss';
import './drawer.js'; import './surround-thumbnails.js';
interface FrigateCardGroupData { interface FrigateCardGroupData {
id: string; id: string;
@@ -155,6 +150,53 @@ export class FrigateCardTimeline extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected cameras?: Map<string, CameraConfig>; protected cameras?: Map<string, CameraConfig>;
@property({ attribute: false })
protected timelineConfig?: TimelineConfig;
/**
* Master render method.
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
if (!this.timelineConfig) {
return html``;
}
return html` <frigate-card-surround-thumbnails
.hass=${this.hass}
.view=${this.view}
.config=${this.timelineConfig.controls.thumbnails}
.targetView=${'clip'}
>
<frigate-card-timeline-core
.hass=${this.hass}
.view=${this.view}
.cameras=${this.cameras}
.timelineConfig=${this.timelineConfig}
>
</frigate-card-timeline-core>
</frigate-card-surround-thumbnails>`;
}
/**
* Return compiled CSS styles.
*/
static get styles(): CSSResultGroup {
return unsafeCSS(timelineStyle);
}
}
@customElement('frigate-card-timeline-core')
export class FrigateCardTimelineCore extends LitElement {
@property({ attribute: false })
protected hass?: HomeAssistant & ExtendedHomeAssistant;
@property({ attribute: false })
protected view?: Readonly<View>;
@property({ attribute: false })
protected cameras?: Map<string, CameraConfig>;
/** /**
* Set the timeline configuration. * Set the timeline configuration.
*/ */
@@ -169,9 +211,7 @@ export class FrigateCardTimeline extends LitElement {
@state({ hasChanged: contentsChanged }) @state({ hasChanged: contentsChanged })
protected _timelineOptions?: TimelineOptions; protected _timelineOptions?: TimelineOptions;
protected _drawerRef: Ref<FrigateCardDrawer> = createRef();
protected _timelineRef: Ref<HTMLElement> = createRef(); protected _timelineRef: Ref<HTMLElement> = createRef();
protected _thumbnailsRef: Ref<FrigateCardThumbnailCarousel> = createRef();
protected _timeline?: Timeline; protected _timeline?: Timeline;
protected _events = new TimelineEventManager(); protected _events = new TimelineEventManager();
@@ -186,68 +226,22 @@ export class FrigateCardTimeline extends LitElement {
} }
const thumbnailsConfig = this._timelineConfig.controls.thumbnails; const thumbnailsConfig = this._timelineConfig.controls.thumbnails;
const drawer = ['left', 'right'].includes(thumbnailsConfig.mode)
? (thumbnailsConfig.mode as 'left' | 'right')
: null;
const timelineClasses = { const timelineClasses = {
timeline: true, timeline: true,
'left-margin': drawer === 'left', 'left-margin': thumbnailsConfig.mode === 'left',
'right-margin': drawer === 'right', 'right-margin': thumbnailsConfig.mode === 'right',
}; };
const renderThumbnails = (): TemplateResult => { return html`<div
const renderCarousel = (): TemplateResult => { class="${classMap(timelineClasses)}"
return html` ${ref(this._timelineRef)}
<frigate-card-thumbnail-carousel ></div>`;
${ref(this._thumbnailsRef)}
.config=${thumbnailsConfig}
.highlight_selected=${true}
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
if (ev.detail.target && ev.detail.childIndex) {
this.view
?.evolve({
target: ev.detail.target,
childIndex: ev.detail.childIndex,
view: 'clip',
})
.dispatchChangeEvent(this);
}
}}
>
</frigate-card-thumbnail-carousel>
`;
};
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() : ''}
`;
} }
/** /**
* Component connected callback. * Handle a range change in the timeline.
* @param properties vis.js provided range information.
*/ */
connectedCallback(): void {
super.connectedCallback();
}
/**
* Component disconnected callback.
*/
disconnectedCallback(): void {
super.disconnectedCallback();
}
protected _timelineRangeHandler(properties: { protected _timelineRangeHandler(properties: {
start: Date; start: Date;
end: Date; end: Date;
@@ -274,15 +268,13 @@ export class FrigateCardTimeline extends LitElement {
/** /**
* Called when an object on the timeline is selected. * Called when an object on the timeline is selected.
* @param data The data about the selection. * @param _data The data about the selection.
* @returns * @returns
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
protected _timelineSelectHandler(_data: { items: string[]; event: Event }): void { protected _timelineSelectHandler(_data: { items: string[]; event: Event }): void {
this._updateThumbnails(); this._updateThumbnails();
if (this._drawerRef.value) { dispatchFrigateCardEvent(this, 'thumbnails:open');
this._drawerRef.value.open = true;
}
} }
protected _updateThumbnails(): void { protected _updateThumbnails(): void {
@@ -337,10 +329,10 @@ export class FrigateCardTimeline extends LitElement {
children: children, children: children,
}; };
if (this._thumbnailsRef.value) { dispatchFrigateCardEvent(this, 'thumbnails:set', {
this._thumbnailsRef.value.target = target; target: target,
this._thumbnailsRef.value.selected = childIndex ?? undefined; childIndex: childIndex ?? undefined,
} });
} }
/** /**
@@ -463,7 +455,7 @@ export class FrigateCardTimeline extends LitElement {
/** /**
* Called when the component is updated. * Called when the component is updated.
* @param changedProps The changed properties if any. * @param changedProperties The changed properties if any.
*/ */
protected updated(changedProperties: PropertyValues): void { protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties); super.updated(changedProperties);
@@ -489,6 +481,6 @@ export class FrigateCardTimeline extends LitElement {
* Return compiled CSS styles. * Return compiled CSS styles.
*/ */
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return unsafeCSS(timelineStyle); return unsafeCSS(timelineCoreStyle);
} }
} }
+5
View File
@@ -0,0 +1,5 @@
:host {
width: 100%;
height: 100%;
display: block;
}
+17
View File
@@ -0,0 +1,17 @@
:host {
width: 100%;
height: 100%;
// Share the screen space with thumbnails that may be above/below.
display: flex;
flex-direction: column;
// So the drawer is relative to this host.
position: relative;
}
::slotted:not([name]) {
// Expand the main body to fill available content not otherwise used by the
// surround.
flex: 1;
}
+88
View File
@@ -0,0 +1,88 @@
@use 'vis-timeline/dist/vis-timeline-graph2d.css';
@use 'drawer';
:host {
width: 100%;
height: 100%;
background-color: var(--card-background-color);
padding-bottom: 5px;
// Share the screen space with thumbnails that may be above/below.
display: flex;
flex-direction: column;
// So that absolute sidedrawer is relative to this host.
position: relative;
}
div.timeline {
flex: 1;
}
div.timeline.left-margin {
// Clearance for the drawer button.
margin-left: calc(drawer.$drawer-icon-size + 1px);
}
div.timeline.right-margin {
// Clearance for the drawer button.
margin-right: calc(drawer.$drawer-icon-size + 1px);
}
.vis-text {
color: var(--primary-text-color) !important;
}
.vis-timeline {
border: none;
}
.vis-labelset .vis-label {
// Group labels.
color: var(--primary-text-color);
}
.vis-item {
border-color: var(--primary-color);
background: none;
color: var(--primary-text-color);
background-color: var(--primary-color);
}
.vis-item:hover {
// Float icons upwards when the user hovers over them.
z-index: 2;
}
.vis-item.vis-box {
border: none;
}
.vis-item .vis-item-content {
padding: 0px;
}
.vis-item.vis-cluster {
border-style: dotted;
color: var(--primary-text-color);
background-color: var(--primary-background-color);
box-shadow: 0px 0px 5px 1px var(--primary-color);
}
.vis-time-axis .vis-grid.vis-minor {
border-color: var(--secondary-color);
}
.vis-time-axis .vis-grid.vis-major {
border-color: var(--secondary-color);
}
.vis-label {
display: flex;
justify-content: center;
align-items: center;
}
div.vis-tooltip {
padding: 0px;
background-color: unset;
border: none;
}
+1 -84
View File
@@ -1,88 +1,5 @@
@use 'vis-timeline/dist/vis-timeline-graph2d.css';
@use 'drawer';
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: var(--card-background-color); display: block;
padding-bottom: 5px;
// Share the screen space with thumbnails that may be above/below.
display: flex;
flex-direction: column;
// So that absolute sidedrawer is relative to this host.
position: relative;
}
div.timeline {
flex: 1;
}
div.timeline.left-margin {
// Clearance for the drawer button.
margin-left: calc(drawer.$drawer-icon-size + 1px);
}
div.timeline.right-margin {
// Clearance for the drawer button.
margin-right: calc(drawer.$drawer-icon-size + 1px);
}
.vis-text {
color: var(--primary-text-color) !important;
}
.vis-timeline {
border: none;
}
.vis-labelset .vis-label {
// Group labels.
color: var(--primary-text-color);
}
.vis-item {
border-color: var(--primary-color);
background: none;
color: var(--primary-text-color);
background-color: var(--primary-color);
}
.vis-item:hover {
// Float icons upwards when the user hovers over them.
z-index: 2;
}
.vis-item.vis-box {
border: none;
}
.vis-item .vis-item-content {
padding: 0px;
}
.vis-item.vis-cluster {
border-style: dotted;
color: var(--primary-text-color);
background-color: var(--primary-background-color);
box-shadow: 0px 0px 5px 1px var(--primary-color);
}
.vis-time-axis .vis-grid.vis-minor {
border-color: var(--secondary-color);
}
.vis-time-axis .vis-grid.vis-major {
border-color: var(--secondary-color);
}
.vis-label {
display: flex;
justify-content: center;
align-items: center;
}
div.vis-tooltip {
padding: 0px;
background-color: unset;
border: none;
} }