Change view to null for unspecified attributes.

This commit is contained in:
Dermot Duffy
2022-04-08 19:54:58 -07:00
parent 9c372815f2
commit f20bd8202f
9 changed files with 109 additions and 94 deletions
+6 -2
View File
@@ -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({ buttons.push({
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.download'), title: localize('config.menu.buttons.download'),
@@ -743,7 +747,7 @@ export class FrigateCard extends LitElement {
* Download media being displayed in the viewer. * Download media being displayed in the viewer.
*/ */
protected async _downloadViewerMedia(): Promise<void> { protected async _downloadViewerMedia(): Promise<void> {
if (!this._hass || !this._view?.isViewerView()) { if (!this._hass || !(this._view?.isViewerView() || this._view?.is('timeline'))) {
// Should not occur. // Should not occur.
return; return;
} }
-1
View File
@@ -214,7 +214,6 @@ export class FrigateCardGalleryCore extends LitElement {
.evolve({ .evolve({
view: this.view.is('clips') ? 'clip' : 'snapshot', view: this.view.is('clips') ? 'clip' : 'snapshot',
childIndex: index, childIndex: index,
previous: this.view,
}) })
.dispatchChangeEvent(this); .dispatchChangeEvent(this);
} }
+4 -1
View File
@@ -357,7 +357,10 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
this.view this.view
.evolve({ .evolve({
camera: Array.from(this.cameras.keys())[selectedSnap], 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); .dispatchChangeEvent(this);
} }
+24 -21
View File
@@ -60,7 +60,7 @@ export class FrigateCardSurround extends LitElement {
view = view as Readonly<View>; view = view as Readonly<View>;
browseMediaParams = browseMediaParams as BrowseMediaQueryParameters; browseMediaParams = browseMediaParams as BrowseMediaQueryParameters;
if (!hass || !view || !browseMediaParams) { if (!hass || !view || view.target || !browseMediaParams) {
return; return;
} }
let parent: FrigateBrowseMediaSource | null; let parent: FrigateBrowseMediaSource | null;
@@ -74,12 +74,20 @@ export class FrigateCardSurround extends LitElement {
?.evolve({ ?.evolve({
...(this.targetView && { view: this.targetView }), ...(this.targetView && { view: this.targetView }),
target: parent, target: parent,
childIndex: undefined, childIndex: null,
}) })
.dispatchChangeEvent(this); .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. * Master render method.
* @returns A rendered template. * @returns A rendered template.
@@ -89,19 +97,22 @@ export class FrigateCardSurround extends LitElement {
return; return;
} }
return html` <frigate-card-surround const changeDrawer = (ev: CustomEvent, action: 'open' | 'close') => {
@frigate-card:thumbnails:open=${(ev: CustomEvent) => { // The event catch/re-dispatch below protect encapsulation: Catches the
if (this.config && ['left', 'right'].includes(this.config.mode)) { // request to view thumbnails and re-dispatches a request to open the drawer
// Protects encapsulation: Catches the request to view thumbnails and // (if the thumbnails are in a drawer). The new event needs to be dispatched
// re-dispatches a request to open the drawer (if the thumbnails are // from the origin of the inbound event, so it can be handled by
// 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> . // <frigate-card-surround> .
dispatchFrigateCardEvent(ev.composedPath()[0], 'drawer:open', { if (this.config && this._hasDrawer()) {
dispatchFrigateCardEvent(ev.composedPath()[0], 'drawer:' + action, {
drawer: this.config.mode, drawer: this.config.mode,
}); });
} }
}} };
return html` <frigate-card-surround
@frigate-card:thumbnails:open=${(ev: CustomEvent) => changeDrawer(ev, 'open')}
@frigate-card:thumbnails:close=${(ev: CustomEvent) => changeDrawer(ev, 'close')}
> >
${this.config?.mode !== 'none' ${this.config?.mode !== 'none'
? html` <frigate-card-thumbnail-carousel ? html` <frigate-card-thumbnail-carousel
@@ -109,16 +120,8 @@ export class FrigateCardSurround extends LitElement {
.config=${this.config} .config=${this.config}
.view=${this.view} .view=${this.view}
.target=${this.view.target} .target=${this.view.target}
.selected=${this.view.childIndex ?? null} .selected=${this.view.childIndex}
@frigate-card:change-view=${(ev) => { @frigate-card:change-view=${(ev: CustomEvent) => changeDrawer(ev, 'close')}
// 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,
});
}
}}
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => { @frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
this.view this.view
?.evolve({ ?.evolve({
+1 -1
View File
@@ -36,7 +36,7 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
// Use contentsChanged here to avoid the carousel rebuilding and resetting in // Use contentsChanged here to avoid the carousel rebuilding and resetting in
// front of the user, unless the contents have actually changed. // front of the user, unless the contents have actually changed.
@property({ attribute: false, hasChanged: contentsChanged }) @property({ attribute: false, hasChanged: contentsChanged })
public target?: FrigateBrowseMediaSource; public target?: FrigateBrowseMediaSource | null;
// Thumbnail carousels can expand (e.g. drawer-based carousels after the main // 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 // media loads). The carousel must be re-initialized in these cases, or the
+1 -1
View File
@@ -90,7 +90,7 @@ export class FrigateCardThumbnail extends LitElement {
?.evolve({ ?.evolve({
view: 'timeline', view: 'timeline',
target: this.target, target: this.target,
childIndex: this.childIndex, childIndex: this.childIndex ?? null,
context: {}, context: {},
}) })
.dispatchChangeEvent(this); .dispatchChangeEvent(this);
+17 -15
View File
@@ -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: Hover over an event should show something useful.
// TODO: Periodically refetch events. // TODO: Periodically refetch events.
// TODO: Search for TODOs and logging statements. // TODO: Search for TODOs and logging statements.
// TODO: Allow download of selected event in timeline.
import { import {
CSSResultGroup, CSSResultGroup,
@@ -211,9 +209,8 @@ class TimelineEventManager {
if (!cameraConfig || !this._dateStart || !this._dateEnd) { if (!cameraConfig || !this._dateStart || !this._dateEnd) {
return; return;
} }
const browseMediaQueryParametersBase = BrowseMediaUtil.getBrowseMediaQueryParametersBase( const browseMediaQueryParametersBase =
cameraConfig, BrowseMediaUtil.getBrowseMediaQueryParametersBase(cameraConfig);
);
if (!browseMediaQueryParametersBase) { if (!browseMediaQueryParametersBase) {
return; return;
} }
@@ -384,20 +381,27 @@ export class FrigateCardTimelineCore extends LitElement {
*/ */
// 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 {
if (!this._thumbnails || !this._thumbnails.children || data.items.length <= 0) { if (!this._thumbnails || !this._thumbnails.children) {
return; return;
} }
const childIndex = this._thumbnails.children.findIndex(
const childIndex = data.items.length
? this._thumbnails.children.findIndex(
(child) => child.frigate?.event.id === data.items[0], (child) => child.frigate?.event.id === data.items[0],
); )
if (childIndex >= 0) { : null;
this.view this.view
?.evolve({ ?.evolve({
target: this._thumbnails, target: this._thumbnails,
childIndex: childIndex, childIndex: childIndex,
}) })
.dispatchChangeEvent(this); .dispatchChangeEvent(this);
if (childIndex !== null && childIndex >= 0) {
dispatchFrigateCardEvent(this, 'thumbnails:open'); dispatchFrigateCardEvent(this, 'thumbnails:open');
} else {
dispatchFrigateCardEvent(this, 'thumbnails:close');
} }
} }
@@ -428,12 +432,12 @@ export class FrigateCardTimelineCore extends LitElement {
['all', 'snapshots'].includes(this.timelineConfig.media) && ['all', 'snapshots'].includes(this.timelineConfig.media) &&
BrowseMediaUtil.isTrueMedia(item.snapshot) BrowseMediaUtil.isTrueMedia(item.snapshot)
) { ) {
added = true added = true;
children.push(item.snapshot); children.push(item.snapshot);
} }
if (added && selected.includes(item.event.id)) { 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 this.view
?.evolve({ ?.evolve({
target: this._thumbnails, target: this._thumbnails,
childIndex: childIndex < 0 ? undefined : childIndex, childIndex: childIndex < 0 ? null : childIndex,
}) })
.dispatchChangeEvent(this); .dispatchChangeEvent(this);
} }
@@ -642,9 +646,7 @@ export class FrigateCardTimelineCore extends LitElement {
}); });
const timelineWindow = this._timeline.getWindow(); const timelineWindow = this._timeline.getWindow();
const context = this.view.context const context = this.view.context as TimelineViewContext | null;
? (this.view.context as TimelineViewContext)
: undefined;
if (context?.window) { if (context?.window) {
console.info( console.info(
+19 -17
View File
@@ -145,11 +145,11 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
// A task to resolve target media if lazy loading is disabled. // A task to resolve target media if lazy loading is disabled.
protected _mediaResolutionTask = new Task< protected _mediaResolutionTask = new Task<
[FrigateBrowseMediaSource | undefined], [FrigateBrowseMediaSource | null | undefined],
void void
>( >(
this, this,
async ([target]: (FrigateBrowseMediaSource | undefined)[]): Promise<void> => { async ([target]: (FrigateBrowseMediaSource | null | undefined)[]): Promise<void> => {
for ( for (
let i = 0; let i = 0;
!this.viewerConfig?.lazy_load && !this.viewerConfig?.lazy_load &&
@@ -183,12 +183,12 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
if (this._carousel && changedProperties.has('view')) { if (this._carousel && changedProperties.has('view')) {
const oldView = changedProperties.get('view') as View | undefined; const oldView = changedProperties.get('view') as View | undefined;
if (oldView) { if (oldView) {
if (oldView.target != this.view?.target) { if (oldView.target !== this.view?.target) {
// If the media target is different entirely, reset the carousel. // If the media target is different entirely, reset the carousel.
this._destroyCarousel(); this._destroyCarousel();
} else if (this.view?.childIndex != oldView.childIndex) { } else if (this.view.childIndex != oldView.childIndex) {
const slide = this._getSlideForChild(this.view?.childIndex); const slide = this._getSlideForChild(this.view.childIndex);
if (slide !== undefined && slide !== this.carouselSelected()) { if (slide !== null && slide !== this.carouselSelected()) {
// If the media target is the same as already loaded, but isn't of // If the media target is the same as already loaded, but isn't of
// the selected slide, scroll to that slide. // the selected slide, scroll to that slide.
this.carouselScrollTo(slide); this.carouselScrollTo(slide);
@@ -226,14 +226,19 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
// need to be destroyed here. // need to be destroyed here.
} }
protected _getSlideForChild(childIndex: number | undefined): number | undefined { /**
if (childIndex === undefined) { * Get the slide number given a media child number.
return undefined; * @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( const slideIndex = Object.keys(this._slideToChild).find(
(key) => this._slideToChild[key] === childIndex, (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 { protected _getOptions(): EmblaOptionsType {
return { return {
// Start the carousel on the selected child number. // 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, draggable: this.viewerConfig?.draggable,
}; };
} }
@@ -286,7 +291,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
!this.view || !this.view ||
!this.view.target || !this.view.target ||
!this.view.target.children || !this.view.target.children ||
this.view.childIndex === undefined this.view.childIndex === null
) { ) {
return null; return null;
} }
@@ -398,12 +403,10 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
} }
const clipStartTime = BrowseMediaUtil.getEventStartTime(child); const clipStartTime = BrowseMediaUtil.getEventStartTime(child);
if (clipStartTime && clipStartTime === snapshotStartTime) { if (clipStartTime && clipStartTime === snapshotStartTime) {
return new View({ return this.view.evolve({
view: 'clip', view: 'clip',
camera: this.view.camera,
target: clips, target: clips,
childIndex: i, childIndex: i,
previous: this.view,
}); });
} }
} }
@@ -426,7 +429,6 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
this.view this.view
.evolve({ .evolve({
childIndex: childIndex, childIndex: childIndex,
previous: this.view,
}) })
.dispatchChangeEvent(this); .dispatchChangeEvent(this);
} }
@@ -443,7 +445,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
const childIndex: number | undefined = this._slideToChild[index]; const childIndex: number | undefined = this._slideToChild[index];
if ( if (
childIndex == undefined || childIndex === undefined ||
!this.hass || !this.hass ||
!this.view || !this.view ||
!this.view.target || !this.view.target ||
+27 -25
View File
@@ -7,10 +7,10 @@ export interface ViewContext {}
export interface ViewEvolveParameters { export interface ViewEvolveParameters {
view?: FrigateCardView; view?: FrigateCardView;
camera?: string; camera?: string;
target?: FrigateBrowseMediaSource; target?: FrigateBrowseMediaSource | null;
childIndex?: number; childIndex?: number | null;
previous?: View; previous?: View | null;
context?: ViewContext; context?: ViewContext | null;
} }
export interface ViewParameters extends ViewEvolveParameters { export interface ViewParameters extends ViewEvolveParameters {
@@ -21,18 +21,18 @@ export interface ViewParameters extends ViewEvolveParameters {
export class View { export class View {
view: FrigateCardView; view: FrigateCardView;
camera: string; camera: string;
target?: FrigateBrowseMediaSource; target: FrigateBrowseMediaSource | null;
childIndex?: number; childIndex: number | null;
previous?: View; previous: View | null;
context?: ViewContext; context: ViewContext | null;
constructor(params: ViewParameters) { constructor(params: ViewParameters) {
this.view = params?.view; this.view = params.view;
this.camera = params?.camera; this.camera = params.camera;
this.target = params?.target; this.target = params.target ?? null;
this.childIndex = params?.childIndex; this.childIndex = params.childIndex ?? null;
this.previous = params?.previous; this.previous = params.previous ?? null;
this.context = params?.context; this.context = params.context ?? null;
} }
/** /**
@@ -56,12 +56,15 @@ export class View {
*/ */
public evolve(params: ViewEvolveParameters): View { public evolve(params: ViewEvolveParameters): View {
return new View({ return new View({
view: params.view ?? this.view, view: params.view !== undefined ? params.view : this.view,
camera: params.camera ?? this.camera, camera: params.camera !== undefined ? params.camera : this.camera,
target: params.target ?? this.target, target: params.target !== undefined ? params.target : this.target,
childIndex: params.childIndex ?? this.childIndex, childIndex: params.childIndex !== undefined ? params.childIndex : this.childIndex,
previous: params.previous ?? this.previous, context: params.context !== undefined ? params.context : this.context,
context: 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 the media item that should be played.
**/ **/
get media(): FrigateBrowseMediaSource | undefined { get media(): FrigateBrowseMediaSource | null {
if (this.target) { if (this.target) {
if (this.target.children && this.childIndex !== undefined) { if (this.target.children && this.childIndex !== null) {
return this.target.children[this.childIndex]; return this.target.children[this.childIndex] ?? null;
} }
return this.target;
} }
return undefined; return null;
} }
/** /**