From f20bd8202f8777660e815777fe8ee96c20ba250d Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Fri, 8 Apr 2022 19:54:58 -0700 Subject: [PATCH] Change view to null for unspecified attributes. --- src/card.ts | 8 +++-- src/components/gallery.ts | 1 - src/components/live.ts | 5 ++- src/components/surround-thumbnails.ts | 51 +++++++++++++------------- src/components/thumbnail-carousel.ts | 2 +- src/components/thumbnail.ts | 2 +- src/components/timeline.ts | 46 ++++++++++++------------ src/components/viewer.ts | 36 ++++++++++--------- src/view.ts | 52 ++++++++++++++------------- 9 files changed, 109 insertions(+), 94 deletions(-) diff --git a/src/card.ts b/src/card.ts index 5d72ba87..c5e41d50 100644 --- a/src/card.ts +++ b/src/card.ts @@ -381,7 +381,11 @@ export class FrigateCard extends LitElement { }); } - if (this._getConfig().menu.buttons.download && this._view?.isViewerView()) { + if ( + this._getConfig().menu.buttons.download && + (this._view?.isViewerView() || this._view?.is('timeline') && + !!this._view?.media) + ) { buttons.push({ type: 'custom:frigate-card-menu-icon', title: localize('config.menu.buttons.download'), @@ -743,7 +747,7 @@ export class FrigateCard extends LitElement { * Download media being displayed in the viewer. */ protected async _downloadViewerMedia(): Promise { - if (!this._hass || !this._view?.isViewerView()) { + if (!this._hass || !(this._view?.isViewerView() || this._view?.is('timeline'))) { // Should not occur. return; } diff --git a/src/components/gallery.ts b/src/components/gallery.ts index c2df5dac..69a5d871 100644 --- a/src/components/gallery.ts +++ b/src/components/gallery.ts @@ -214,7 +214,6 @@ export class FrigateCardGalleryCore extends LitElement { .evolve({ view: this.view.is('clips') ? 'clip' : 'snapshot', childIndex: index, - previous: this.view, }) .dispatchChangeEvent(this); } diff --git a/src/components/live.ts b/src/components/live.ts index 7285fbd2..f4612078 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -357,7 +357,10 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { this.view .evolve({ camera: Array.from(this.cameras.keys())[selectedSnap], - previous: this.view, + + // Reset the target so thumbnails will be re-fetched. + target: null, + childIndex: null, }) .dispatchChangeEvent(this); } diff --git a/src/components/surround-thumbnails.ts b/src/components/surround-thumbnails.ts index 466e256d..835bde6d 100644 --- a/src/components/surround-thumbnails.ts +++ b/src/components/surround-thumbnails.ts @@ -60,7 +60,7 @@ export class FrigateCardSurround extends LitElement { view = view as Readonly; browseMediaParams = browseMediaParams as BrowseMediaQueryParameters; - if (!hass || !view || !browseMediaParams) { + if (!hass || !view || view.target || !browseMediaParams) { return; } let parent: FrigateBrowseMediaSource | null; @@ -74,12 +74,20 @@ export class FrigateCardSurround extends LitElement { ?.evolve({ ...(this.targetView && { view: this.targetView }), target: parent, - childIndex: undefined, + childIndex: null, }) .dispatchChangeEvent(this); } } + /** + * Determine if a drawer is being used. + * @returns `true` if a drawer is used, `false` otherwise. + */ + protected _hasDrawer(): boolean { + return !!this.config && ['left', 'right'].includes(this.config.mode); + } + /** * Master render method. * @returns A rendered template. @@ -89,19 +97,22 @@ export class FrigateCardSurround extends LitElement { return; } + const changeDrawer = (ev: CustomEvent, action: 'open' | 'close') => { + // The event catch/re-dispatch below protect 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 + // . + if (this.config && this._hasDrawer()) { + dispatchFrigateCardEvent(ev.composedPath()[0], 'drawer:' + action, { + drawer: this.config.mode, + }); + } + }; + return html` { - 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, - }); - } - }} + @frigate-card:thumbnails:open=${(ev: CustomEvent) => changeDrawer(ev, 'open')} + @frigate-card:thumbnails:close=${(ev: CustomEvent) => changeDrawer(ev, 'close')} > ${this.config?.mode !== 'none' ? html` { - // Close the drawer if the carousel or thumbnail requests a view change - // (e.g. playing the clip, or viewing something on the timeline). - if (this.config && ['left', 'right'].includes(this.config.mode)) { - dispatchFrigateCardEvent(ev.composedPath()[0], 'drawer:close', { - drawer: this.config.mode, - }); - } - }} + .selected=${this.view.childIndex} + @frigate-card:change-view=${(ev: CustomEvent) => changeDrawer(ev, 'close')} @frigate-card:carousel:tap=${(ev: CustomEvent) => { this.view ?.evolve({ diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts index 556d56a8..28fedd9f 100644 --- a/src/components/thumbnail-carousel.ts +++ b/src/components/thumbnail-carousel.ts @@ -36,7 +36,7 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { // Use contentsChanged here to avoid the carousel rebuilding and resetting in // front of the user, unless the contents have actually changed. @property({ attribute: false, hasChanged: contentsChanged }) - public target?: FrigateBrowseMediaSource; + public target?: FrigateBrowseMediaSource | null; // Thumbnail carousels can expand (e.g. drawer-based carousels after the main // media loads). The carousel must be re-initialized in these cases, or the diff --git a/src/components/thumbnail.ts b/src/components/thumbnail.ts index 2f870ffe..0ba65552 100644 --- a/src/components/thumbnail.ts +++ b/src/components/thumbnail.ts @@ -90,7 +90,7 @@ export class FrigateCardThumbnail extends LitElement { ?.evolve({ view: 'timeline', target: this.target, - childIndex: this.childIndex, + childIndex: this.childIndex ?? null, context: {}, }) .dispatchChangeEvent(this); diff --git a/src/components/timeline.ts b/src/components/timeline.ts index c4f0aa6d..dba50bf1 100644 --- a/src/components/timeline.ts +++ b/src/components/timeline.ts @@ -1,8 +1,6 @@ -// TODO: Clips vs snapshots: Should be able to navigate from snapshots view and it should just work. // TODO: Hover over an event should show something useful. // TODO: Periodically refetch events. // TODO: Search for TODOs and logging statements. -// TODO: Allow download of selected event in timeline. import { CSSResultGroup, @@ -211,9 +209,8 @@ class TimelineEventManager { if (!cameraConfig || !this._dateStart || !this._dateEnd) { return; } - const browseMediaQueryParametersBase = BrowseMediaUtil.getBrowseMediaQueryParametersBase( - cameraConfig, - ); + const browseMediaQueryParametersBase = + BrowseMediaUtil.getBrowseMediaQueryParametersBase(cameraConfig); if (!browseMediaQueryParametersBase) { return; } @@ -384,20 +381,27 @@ export class FrigateCardTimelineCore extends LitElement { */ // eslint-disable-next-line @typescript-eslint/no-unused-vars protected _timelineSelectHandler(data: { items: string[]; event: Event }): void { - if (!this._thumbnails || !this._thumbnails.children || data.items.length <= 0) { + if (!this._thumbnails || !this._thumbnails.children) { return; } - const childIndex = this._thumbnails.children.findIndex( - (child) => child.frigate?.event.id === data.items[0], - ); - if (childIndex >= 0) { - this.view - ?.evolve({ - target: this._thumbnails, - childIndex: childIndex, - }) - .dispatchChangeEvent(this); + + const childIndex = data.items.length + ? this._thumbnails.children.findIndex( + (child) => child.frigate?.event.id === data.items[0], + ) + : null; + + this.view + ?.evolve({ + target: this._thumbnails, + childIndex: childIndex, + }) + .dispatchChangeEvent(this); + + if (childIndex !== null && childIndex >= 0) { dispatchFrigateCardEvent(this, 'thumbnails:open'); + } else { + dispatchFrigateCardEvent(this, 'thumbnails:close'); } } @@ -428,12 +432,12 @@ export class FrigateCardTimelineCore extends LitElement { ['all', 'snapshots'].includes(this.timelineConfig.media) && BrowseMediaUtil.isTrueMedia(item.snapshot) ) { - added = true + added = true; children.push(item.snapshot); } if (added && selected.includes(item.event.id)) { - childIndex = children.length-1; + childIndex = children.length - 1; } } }); @@ -459,7 +463,7 @@ export class FrigateCardTimelineCore extends LitElement { this.view ?.evolve({ target: this._thumbnails, - childIndex: childIndex < 0 ? undefined : childIndex, + childIndex: childIndex < 0 ? null : childIndex, }) .dispatchChangeEvent(this); } @@ -642,9 +646,7 @@ export class FrigateCardTimelineCore extends LitElement { }); const timelineWindow = this._timeline.getWindow(); - const context = this.view.context - ? (this.view.context as TimelineViewContext) - : undefined; + const context = this.view.context as TimelineViewContext | null; if (context?.window) { console.info( diff --git a/src/components/viewer.ts b/src/components/viewer.ts index 90de4107..d01f544c 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -145,11 +145,11 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { // A task to resolve target media if lazy loading is disabled. protected _mediaResolutionTask = new Task< - [FrigateBrowseMediaSource | undefined], + [FrigateBrowseMediaSource | null | undefined], void >( this, - async ([target]: (FrigateBrowseMediaSource | undefined)[]): Promise => { + async ([target]: (FrigateBrowseMediaSource | null | undefined)[]): Promise => { for ( let i = 0; !this.viewerConfig?.lazy_load && @@ -183,12 +183,12 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { if (this._carousel && changedProperties.has('view')) { const oldView = changedProperties.get('view') as View | undefined; if (oldView) { - if (oldView.target != this.view?.target) { + if (oldView.target !== this.view?.target) { // If the media target is different entirely, reset the carousel. this._destroyCarousel(); - } else if (this.view?.childIndex != oldView.childIndex) { - const slide = this._getSlideForChild(this.view?.childIndex); - if (slide !== undefined && slide !== this.carouselSelected()) { + } else if (this.view.childIndex != oldView.childIndex) { + const slide = this._getSlideForChild(this.view.childIndex); + if (slide !== null && slide !== this.carouselSelected()) { // If the media target is the same as already loaded, but isn't of // the selected slide, scroll to that slide. this.carouselScrollTo(slide); @@ -226,14 +226,19 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { // need to be destroyed here. } - protected _getSlideForChild(childIndex: number | undefined): number | undefined { - if (childIndex === undefined) { - return undefined; + /** + * Get the slide number given a media child number. + * @param childIndex The child index (relative to `view.target`) + * @returns A number or null if the child is not found. + */ + protected _getSlideForChild(childIndex: number | null | undefined): number | null { + if (childIndex === undefined || childIndex === null) { + return null; } const slideIndex = Object.keys(this._slideToChild).find( (key) => this._slideToChild[key] === childIndex, ); - return slideIndex !== undefined ? Number(slideIndex) : undefined; + return slideIndex !== undefined ? Number(slideIndex) : null; } /** @@ -251,7 +256,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { protected _getOptions(): EmblaOptionsType { return { // Start the carousel on the selected child number. - startIndex: this._getSlideForChild(this.view?.childIndex), + startIndex: this._getSlideForChild(this.view?.childIndex) ?? undefined, draggable: this.viewerConfig?.draggable, }; } @@ -286,7 +291,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { !this.view || !this.view.target || !this.view.target.children || - this.view.childIndex === undefined + this.view.childIndex === null ) { return null; } @@ -398,12 +403,10 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { } const clipStartTime = BrowseMediaUtil.getEventStartTime(child); if (clipStartTime && clipStartTime === snapshotStartTime) { - return new View({ + return this.view.evolve({ view: 'clip', - camera: this.view.camera, target: clips, childIndex: i, - previous: this.view, }); } } @@ -426,7 +429,6 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { this.view .evolve({ childIndex: childIndex, - previous: this.view, }) .dispatchChangeEvent(this); } @@ -443,7 +445,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { const childIndex: number | undefined = this._slideToChild[index]; if ( - childIndex == undefined || + childIndex === undefined || !this.hass || !this.view || !this.view.target || diff --git a/src/view.ts b/src/view.ts index 8edbba54..42ad6300 100644 --- a/src/view.ts +++ b/src/view.ts @@ -7,10 +7,10 @@ export interface ViewContext {} export interface ViewEvolveParameters { view?: FrigateCardView; camera?: string; - target?: FrigateBrowseMediaSource; - childIndex?: number; - previous?: View; - context?: ViewContext; + target?: FrigateBrowseMediaSource | null; + childIndex?: number | null; + previous?: View | null; + context?: ViewContext | null; } export interface ViewParameters extends ViewEvolveParameters { @@ -21,18 +21,18 @@ export interface ViewParameters extends ViewEvolveParameters { export class View { view: FrigateCardView; camera: string; - target?: FrigateBrowseMediaSource; - childIndex?: number; - previous?: View; - context?: ViewContext; + target: FrigateBrowseMediaSource | null; + childIndex: number | null; + previous: View | null; + context: ViewContext | null; constructor(params: ViewParameters) { - this.view = params?.view; - this.camera = params?.camera; - this.target = params?.target; - this.childIndex = params?.childIndex; - this.previous = params?.previous; - this.context = params?.context; + this.view = params.view; + this.camera = params.camera; + this.target = params.target ?? null; + this.childIndex = params.childIndex ?? null; + this.previous = params.previous ?? null; + this.context = params.context ?? null; } /** @@ -56,12 +56,15 @@ export class View { */ public evolve(params: ViewEvolveParameters): View { return new View({ - view: params.view ?? this.view, - camera: params.camera ?? this.camera, - target: params.target ?? this.target, - childIndex: params.childIndex ?? this.childIndex, - previous: params.previous ?? this.previous, - context: params.context ?? this.context, + view: params.view !== undefined ? params.view : this.view, + camera: params.camera !== undefined ? params.camera : this.camera, + target: params.target !== undefined ? params.target : this.target, + childIndex: params.childIndex !== undefined ? params.childIndex : this.childIndex, + context: params.context !== undefined ? params.context : this.context, + + // Special case: Set the previous to this of the evolved view (rather than + // the previous of this). + previous: params.previous !== undefined ? params.previous : this, }); } @@ -110,14 +113,13 @@ export class View { /** * Get the media item that should be played. **/ - get media(): FrigateBrowseMediaSource | undefined { + get media(): FrigateBrowseMediaSource | null { if (this.target) { - if (this.target.children && this.childIndex !== undefined) { - return this.target.children[this.childIndex]; + if (this.target.children && this.childIndex !== null) { + return this.target.children[this.childIndex] ?? null; } - return this.target; } - return undefined; + return null; } /**