diff --git a/src/common.ts b/src/common.ts index 27bf72e7..4bf4301c 100644 --- a/src/common.ts +++ b/src/common.ts @@ -115,11 +115,11 @@ export async function homeAssistantSignPath( * @param detail An optional detail object to attach. */ export function dispatchFrigateCardEvent( - element: HTMLElement, + target: EventTarget, name: string, detail?: T, ): void { - element.dispatchEvent( + target.dispatchEvent( new CustomEvent(`frigate-card:${name}`, { bubbles: true, composed: true, diff --git a/src/components/drawer.ts b/src/components/drawer.ts index 0ae5c1ee..2945b3f3 100644 --- a/src/components/drawer.ts +++ b/src/components/drawer.ts @@ -21,23 +21,11 @@ export class FrigateCardDrawer extends LitElement { @property({ attribute: true, reflect: true, type: Boolean }) public control = true; - /** - * Set the timeline configuration. - */ @property({ type: Boolean, reflect: true, attribute: true }) - set open(open: boolean) { - if (this._drawerRef.value) { - const old = this._drawerRef.value.open; - this._drawerRef.value.open = open; - this.requestUpdate('open', old); - } - } + public open = false; - get open(): boolean { - return this._drawerRef.value?.open ?? false; - } - - protected _drawerRef: Ref = createRef(); + protected _refDrawer: Ref = createRef(); + protected _refSlot: Ref = createRef(); /** * 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. const style = document.createElement('style'); 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 { return html` - + ${this.control ? html`
` : ''} - + `; } diff --git a/src/components/surround-thumbnails.ts b/src/components/surround-thumbnails.ts new file mode 100644 index 00000000..3283a62b --- /dev/null +++ b/src/components/surround-thumbnails.ts @@ -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; + + @property({ attribute: false }) + protected config?: ThumbnailsControlConfig; + + @property({ attribute: false }) + protected targetView?: FrigateCardView; + + protected _refThumbnails: Ref = createRef(); + + /** + * Master render method. + * @returns A rendered template. + */ + protected render(): TemplateResult | void { + if (!this.hass || !this.view || !this.config) { + return; + } + + return html` ) => { + 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 + // . + dispatchFrigateCardEvent(ev.composedPath()[0], 'drawer:open', { + drawer: this.config.mode, + }); + } + }} + > + ${this.config?.mode !== 'none' + ? html` ) => { + 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); + } + }} + > + ` + : ''} + + `; + } + + /** + * Return compiled CSS styles. + */ + static get styles(): CSSResultGroup { + return unsafeCSS(surroundThumbnailsStyle); + } +} diff --git a/src/components/surround.ts b/src/components/surround.ts new file mode 100644 index 00000000..e929b95e --- /dev/null +++ b/src/components/surround.ts @@ -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 = createRef(); + protected _refDrawerRight: Ref = 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).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` + + + + + + + + `; + } + + /** + * Return compiled CSS styles. + */ + static get styles(): CSSResultGroup { + return unsafeCSS(surroundStyle); + } +} diff --git a/src/components/timeline.ts b/src/components/timeline.ts index 86d79910..f8376a75 100644 --- a/src/components/timeline.ts +++ b/src/components/timeline.ts @@ -23,28 +23,23 @@ import { CameraConfig, ExtendedHomeAssistant, FrigateBrowseMediaSource, - ThumbnailsControlConfig, MEDIA_CLASS_PLAYLIST, MEDIA_TYPE_VIDEO, MEDIA_CLASS_VIDEO, TimelineConfig, - frigateCardConfigDefaults, } from '../types'; -import { FrigateCardDrawer } from './drawer'; -import { - FrigateCardThumbnailCarousel, - ThumbnailCarouselTap, -} from './thumbnail-carousel'; import { View } from '../view'; import { contentsChanged, dispatchErrorMessageEvent, + dispatchFrigateCardEvent, getCameraTitle, } from '../common.js'; +import timelineCoreStyle from '../scss/timeline-core.scss'; import timelineStyle from '../scss/timeline.scss'; -import './drawer.js'; +import './surround-thumbnails.js'; interface FrigateCardGroupData { id: string; @@ -155,6 +150,53 @@ export class FrigateCardTimeline extends LitElement { @property({ attribute: false }) protected cameras?: Map; + @property({ attribute: false }) + protected timelineConfig?: TimelineConfig; + + /** + * Master render method. + * @returns A rendered template. + */ + protected render(): TemplateResult | void { + if (!this.timelineConfig) { + return html``; + } + + return html` + + + `; + } + + /** + * 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; + + @property({ attribute: false }) + protected cameras?: Map; + /** * Set the timeline configuration. */ @@ -169,9 +211,7 @@ export class FrigateCardTimeline extends LitElement { @state({ hasChanged: contentsChanged }) protected _timelineOptions?: TimelineOptions; - protected _drawerRef: Ref = createRef(); protected _timelineRef: Ref = createRef(); - protected _thumbnailsRef: Ref = createRef(); protected _timeline?: Timeline; protected _events = new TimelineEventManager(); @@ -186,68 +226,22 @@ export class FrigateCardTimeline extends LitElement { } const thumbnailsConfig = this._timelineConfig.controls.thumbnails; - - const drawer = ['left', 'right'].includes(thumbnailsConfig.mode) - ? (thumbnailsConfig.mode as 'left' | 'right') - : null; - const timelineClasses = { timeline: true, - 'left-margin': drawer === 'left', - 'right-margin': drawer === 'right', + 'left-margin': thumbnailsConfig.mode === 'left', + 'right-margin': thumbnailsConfig.mode === 'right', }; - 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() : ''} - `; + return html`
`; } /** - * 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: { start: Date; end: Date; @@ -274,15 +268,13 @@ export class FrigateCardTimeline extends LitElement { /** * Called when an object on the timeline is selected. - * @param data The data about the selection. + * @param _data The data about the selection. * @returns */ // eslint-disable-next-line @typescript-eslint/no-unused-vars protected _timelineSelectHandler(_data: { items: string[]; event: Event }): void { this._updateThumbnails(); - if (this._drawerRef.value) { - this._drawerRef.value.open = true; - } + dispatchFrigateCardEvent(this, 'thumbnails:open'); } protected _updateThumbnails(): void { @@ -337,10 +329,10 @@ export class FrigateCardTimeline extends LitElement { children: children, }; - if (this._thumbnailsRef.value) { - this._thumbnailsRef.value.target = target; - this._thumbnailsRef.value.selected = childIndex ?? undefined; - } + dispatchFrigateCardEvent(this, 'thumbnails:set', { + target: target, + childIndex: childIndex ?? undefined, + }); } /** @@ -463,7 +455,7 @@ export class FrigateCardTimeline extends LitElement { /** * 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 { super.updated(changedProperties); @@ -489,6 +481,6 @@ export class FrigateCardTimeline extends LitElement { * Return compiled CSS styles. */ static get styles(): CSSResultGroup { - return unsafeCSS(timelineStyle); + return unsafeCSS(timelineCoreStyle); } } diff --git a/src/scss/surround-thumbnails.scss b/src/scss/surround-thumbnails.scss new file mode 100644 index 00000000..f0f64c07 --- /dev/null +++ b/src/scss/surround-thumbnails.scss @@ -0,0 +1,5 @@ +:host { + width: 100%; + height: 100%; + display: block; +} \ No newline at end of file diff --git a/src/scss/surround.scss b/src/scss/surround.scss new file mode 100644 index 00000000..47236da5 --- /dev/null +++ b/src/scss/surround.scss @@ -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; +} diff --git a/src/scss/timeline-core.scss b/src/scss/timeline-core.scss new file mode 100644 index 00000000..4d90158a --- /dev/null +++ b/src/scss/timeline-core.scss @@ -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; +} diff --git a/src/scss/timeline.scss b/src/scss/timeline.scss index 4d90158a..f0f64c07 100644 --- a/src/scss/timeline.scss +++ b/src/scss/timeline.scss @@ -1,88 +1,5 @@ -@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; -} + display: block; +} \ No newline at end of file