diff --git a/src/card.ts b/src/card.ts index 2ddf493a..523f4cc8 100644 --- a/src/card.ts +++ b/src/card.ts @@ -99,6 +99,7 @@ import { supportsFeature } from './utils/ha/update.js'; import { isValidMediaShowInfo } from './utils/media-info.js'; import { View } from './view.js'; import pkg from '../package.json'; +import { ViewContext } from 'view'; /** A note on media callbacks: * @@ -184,8 +185,9 @@ export class FrigateCard extends LitElement { // Automated refreshes of the default view. protected _updateTimerID: number | null = null; - // Information about the most recently loaded media item. - protected _mediaShowInfo: MediaShowInfo | null = null; + // Information about loaded media items. + protected _currentMediaShowInfo: MediaShowInfo | null = null; + protected _lastValidMediaShowInfo: MediaShowInfo | null = null; // Array of dynamic menu buttons to be added to menu. protected _dynamicMenuButtons: MenuButton[] = []; @@ -331,7 +333,11 @@ export class FrigateCard extends LitElement { for (const action of actions) { // All frigate card actions will have action of 'fire-dom-event' and // styling only applies to those. - if (!action || action.action !== 'fire-dom-event' || !('frigate_card_action' in action)) { + if ( + !action || + action.action !== 'fire-dom-event' || + !('frigate_card_action' in action) + ) { continue; } const frigateCardAction = action as FrigateCardCustomAction; @@ -925,6 +931,15 @@ export class FrigateCard extends LitElement { } protected _changeView(args?: { view?: View; resetMessage?: boolean }): void { + const changeView = (view: View): void => { + if (View.isMediaChange(this._view, view)) { + this._currentMediaShowInfo = null; + } + this._view = view; + this._generateConditionState(); + this._resetMainScroll(); + }; + if (args?.resetMessage ?? true) { this._message = null; } @@ -945,21 +960,19 @@ export class FrigateCard extends LitElement { } if (camera) { - this._view = new View({ - view: this._getConfig().view.default, - camera: camera, - }); - this._generateConditionState(); - this._resetMainScroll(); + changeView( + new View({ + view: this._getConfig().view.default, + camera: camera, + }), + ); // Restart the update timer, so the default view is refreshed at a fixed // interval from now (if so configured). this._startUpdateTimer(); } } else { - this._view = args.view; - this._generateConditionState(); - this._resetMainScroll(); + changeView(args.view); } } @@ -987,6 +1000,15 @@ export class FrigateCard extends LitElement { this._changeView({ view: e.detail }); } + /** + * Add view context to the current view. + * @param ev A ViewContext event. + */ + protected _addViewContextHandler(ev: CustomEvent): void { + this._changeView({ + view: this._view?.clone().mergeInContext(ev.detail), + }); + } /** * Called before each update. */ @@ -1592,7 +1614,7 @@ export class FrigateCard extends LitElement { */ protected _resetMainScroll(): void { // Reset the scroll on the main div to the top. - this._refMain.value?.scroll({top: 0}); + this._refMain.value?.scroll({ top: 0 }); } /** @@ -1614,19 +1636,11 @@ export class FrigateCard extends LitElement { if (!isValidMediaShowInfo(mediaShowInfo)) { return; } - let requestRefresh = false; - if ( - this._view?.isGalleryView() && - (mediaShowInfo.width != this._mediaShowInfo?.width || - mediaShowInfo.height != this._mediaShowInfo?.height) - ) { - requestRefresh = true; - } - this._mediaShowInfo = mediaShowInfo; - if (requestRefresh) { - this.requestUpdate(); - } + this._lastValidMediaShowInfo = this._currentMediaShowInfo = mediaShowInfo; + + // An update may be required to draw elements. + this.requestUpdate(); } /** @@ -1702,8 +1716,8 @@ export class FrigateCard extends LitElement { } const aspectRatioMode = this._getConfig().dimensions.aspect_ratio_mode; - if (aspectRatioMode == 'dynamic' && this._mediaShowInfo) { - return `${this._mediaShowInfo.width} / ${this._mediaShowInfo.height}`; + if (aspectRatioMode == 'dynamic' && this._lastValidMediaShowInfo) { + return `${this._lastValidMediaShowInfo.width} / ${this._lastValidMediaShowInfo.height}`; } const defaultAspectRatio = this._getConfig().dimensions.aspect_ratio; @@ -1776,16 +1790,14 @@ export class FrigateCard extends LitElement { style="${styleMap(cardStyle)}" @action=${(ev: CustomEvent) => this._actionHandler(ev, actions)} @ll-custom=${this._cardActionHandler.bind(this)} - @frigate-card:message=${this._messageHandler} - @frigate-card:change-view=${this._changeViewHandler} + @frigate-card:message=${this._messageHandler.bind(this)} + @frigate-card:view:change=${this._changeViewHandler.bind(this)} + @frigate-card:view:change-context=${this._addViewContextHandler.bind(this)} @frigate-card:media-show=${this._mediaShowHandler} @frigate-card:render=${() => this.requestUpdate()} > ${renderMenuAbove ? this._renderMenu() : ''} -
+
${this._cameras === undefined && !this._message ? until( (async () => { @@ -1807,9 +1819,12 @@ export class FrigateCard extends LitElement { }
${!renderMenuAbove ? this._renderMenu() : ''} - ${!this._message && this._getConfig().elements + ${!this._message && + (!this._view?.isAnyMediaView() || this._currentMediaShowInfo) && + this._getConfig().elements ? // Elements need to render after the main views so it can render 'on - // top'. + // top', but only if the view is a non-media view or the media is + // already loaded. html` ) => { + @frigate-card:view:change=${(ev: CustomEvent) => { if (this._inBackground) { ev.stopPropagation(); } @@ -448,10 +451,13 @@ export class FrigateCardLiveCarousel extends LitElement { .evolve({ camera: Array.from(this.cameras.keys())[selectedCameraIndex], - // Reset the target so thumbnails will be re-fetched. + // Reset the target. target: null, childIndex: null, }) + // Don't yet fetch thumbnails (they will be fetched when the carousel + // settles). + .mergeInContext({ thumbnails: { fetch: false } }) .dispatchChangeEvent(this); } @@ -582,7 +588,11 @@ export class FrigateCardLiveCarousel extends LitElement { .label="${title ? `${localize('common.live')}: ${title}` : ''}" .titlePopupConfig=${config.controls.title} transitionEffect=${this._getTransitionEffect()} - @frigate-card:carousel:settle=${this._setViewHandler.bind(this)} + @frigate-card:media-carousel:select=${this._setViewHandler.bind(this)} + @frigate-card:carousel:settle=${() => { + // Fetch the thumbnails after the carousel has settled. + dispatchViewContextChangeEvent(this, { thumbnails: { fetch: true }}); + }} > ) => { this._slideResizeObserver.disconnect(); this._slideResizeObserver.observe(ev.detail.element); + + // Pass up the media-carousel select event first to allow parents to + // initialize/reset before the media info is dispatched. + dispatchFrigateCardEvent( + this, + 'media-carousel:select', + ev.detail, + ); + + // Dispatch media info. this._dispatchMediaShowInfo(); }} @frigate-card:carousel:media-show=${this._storeMediaShowInfo.bind(this)} diff --git a/src/components/surround-thumbnails.ts b/src/components/surround-thumbnails.ts index 897d081c..2b6978a8 100644 --- a/src/components/surround-thumbnails.ts +++ b/src/components/surround-thumbnails.ts @@ -27,6 +27,17 @@ import { dispatchFrigateCardErrorEvent } from './message.js'; import './surround.js'; import { ThumbnailCarouselTap } from './thumbnail-carousel.js'; +interface ThumbnailViewContext { + // Whetherr or not to fetch thumbnails. + fetch?: boolean; +} + +declare module 'view' { + interface ViewContext { + thumbnails?: ThumbnailViewContext; + } +} + @customElement('frigate-card-surround-thumbnails') export class FrigateCardSurround extends LitElement { @property({ attribute: false }) @@ -64,7 +75,8 @@ export class FrigateCardSurround extends LitElement { !this.config || this.config.mode === 'none' || this.view.target || - !this.browseMediaParams + !this.browseMediaParams || + !(this.view.context?.thumbnails?.fetch ?? true) ) { return; } @@ -147,7 +159,7 @@ export class FrigateCardSurround extends LitElement { .target=${this.view.target} .selected=${this.view.childIndex} .cameras=${this.cameras} - @frigate-card:change-view=${(ev: CustomEvent) => changeDrawer(ev, 'close')} + @frigate-card:view:change=${(ev: CustomEvent) => changeDrawer(ev, 'close')} @frigate-card:thumbnail-carousel:tap=${(ev: CustomEvent) => { // Send the view change from the source of the tap event, so the // view change will be caught by the handler above (to close the drawer). diff --git a/src/components/thumbnail.ts b/src/components/thumbnail.ts index ca224410..30450b60 100644 --- a/src/components/thumbnail.ts +++ b/src/components/thumbnail.ts @@ -282,8 +282,8 @@ export class FrigateCardThumbnail extends LitElement { view: 'timeline', target: this.target, childIndex: this.childIndex ?? null, - context: {}, }) + .removeContext('timeline') .dispatchChangeEvent(this); } else if (recording) { this.view @@ -291,7 +291,9 @@ export class FrigateCardThumbnail extends LitElement { view: 'timeline', target: null, childIndex: null, - context: { + }) + .mergeInContext({ + timeline: { window: { start: fromUnixTime(recording.start_time), end: fromUnixTime(recording.end_time), diff --git a/src/components/timeline.ts b/src/components/timeline.ts index de70feb1..d88cf68d 100644 --- a/src/components/timeline.ts +++ b/src/components/timeline.ts @@ -21,6 +21,7 @@ import { customElement, property } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { createRef, ref, Ref } from 'lit/directives/ref.js'; import { isEqual } from 'lodash-es'; +import { ViewContext } from 'view'; import { DataSet } from 'vis-data/esnext'; import { DataGroupCollectionType, @@ -64,7 +65,7 @@ import { isTrueMedia, multipleBrowseMediaQuery, } from '../utils/ha/browse-media'; -import { View, ViewContext } from '../view'; +import { View } from '../view'; import { dispatchFrigateCardErrorEvent, dispatchMessageEvent } from './message.js'; import './surround-thumbnails.js'; @@ -81,12 +82,18 @@ interface FrigateCardTimelineItem extends TimelineItem { source?: FrigateBrowseMediaSource; } -interface TimelineViewContext extends ViewContext { - // The selected timeline window. - window?: TimelineWindow; +interface TimelineViewContext { + // The selected timeline window. + window?: TimelineWindow; - // The date of the last event fetch. - dateFetch?: Date; + // The date of the last event fetch. + dateFetch?: Date; +} + +declare module 'view' { + interface ViewContext { + timeline?: TimelineViewContext; + } } type TimelineMediaType = 'all' | 'clips' | 'snapshots'; @@ -867,8 +874,8 @@ export class FrigateCardTimelineCore extends LitElement { ?.evolve({ target: thumbnails?.target ?? null, childIndex: thumbnails?.childIndex ?? null, - context: this._generateViewContext(true), }) + .mergeInContext(this._generateTimelineContext(true)) .dispatchChangeEvent(this); } }); @@ -1174,7 +1181,7 @@ export class FrigateCardTimelineCore extends LitElement { // Regenerate the thumbnails after the selection, to allow the new selection // to be in the generated view. - const context = this.view.context as TimelineViewContext | null; + const context = this.view.context?.timeline; const timelineWindow = this._timeline.getWindow(); if (context?.window) { @@ -1222,8 +1229,8 @@ export class FrigateCardTimelineCore extends LitElement { ?.evolve({ target: thumbnails?.target ?? null, childIndex: thumbnails?.childIndex ?? null, - context: this._generateViewContext(false), }) + .mergeInContext(this._generateTimelineContext(false)) .dispatchChangeEvent(this); } } @@ -1234,9 +1241,10 @@ export class FrigateCardTimelineCore extends LitElement { * the window is preserved if it is already in the context. * @returns The TimelineViewContext object. */ - protected _generateViewContext(addWindow: boolean): TimelineViewContext { - const currentContext = this.view?.context as TimelineViewContext | undefined; - const newContext: TimelineViewContext = {}; + protected _generateTimelineContext(addWindow: boolean): ViewContext { + const currentContext = this.view?.context?.timeline; + const newContext: TimelineViewContext = {} + if (addWindow && this._timeline) { newContext.window = this._timeline.getWindow(); } else if (currentContext?.window) { @@ -1245,7 +1253,7 @@ export class FrigateCardTimelineCore extends LitElement { if (this._data.lastFetchDate) { newContext.dateFetch = this._data.lastFetchDate; } - return newContext || null; + return Object.keys(newContext) ? {timeline: newContext} : {}; } /** diff --git a/src/components/viewer.ts b/src/components/viewer.ts index b193f29e..3a932472 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -618,7 +618,7 @@ export class FrigateCardViewerCarousel extends LitElement { .label="${this.view.media.title}" .titlePopupConfig=${this.viewerConfig?.controls.title} transitionEffect=${this._getTransitionEffect()} - @frigate-card:carousel:select=${this._setViewHandler.bind(this)} + @frigate-card:media-carousel:select=${this._setViewHandler.bind(this)} @frigate-card:media-show=${this._recordingSeekHandler.bind(this)} > { + dispatchFrigateCardEvent(target, 'view:change-context', context); +};