Files
advanced-camera-card/src/view/view.ts
T

213 lines
5.7 KiB
TypeScript

import { cloneDeep, merge } from 'lodash-es';
import { ViewContext } from 'view';
import { AdvancedCameraCardView } from '../config/schema/common/const';
import { ViewDisplayMode } from '../config/schema/common/display';
import { ViewMediaType } from '../types';
import { QueryResults } from './query-results';
import { UnifiedQuery } from './unified-query';
declare module 'view' {
interface ViewContext {
// Per-target media retry epoch. Keyed by the universal target ID returned
// by `getViewTargetID` (camera ID / media ID / image sentinel). When bumped
// for a target, consumers rendering that target tear down and recreate
// their media element to retry loading.
mediaEpoch?: Record<string, number>;
loading?: {
query?: unknown;
};
}
}
interface ViewEvolveParameters {
view?: AdvancedCameraCardView;
camera?: string | null;
query?: UnifiedQuery | null;
queryResults?: QueryResults | null;
context?: ViewContext | null;
displayMode?: ViewDisplayMode | null;
}
export interface ViewParameters extends ViewEvolveParameters {
view: AdvancedCameraCardView;
}
export const mergeViewContext = (
a?: ViewContext | null,
b?: ViewContext | null,
): ViewContext => {
return merge({}, a, b);
};
const VIEWER_VIEW_NAMES: readonly AdvancedCameraCardView[] = [
'folder',
'media',
'clip',
'snapshot',
'recording',
'review',
];
const GALLERY_VIEW_NAMES: readonly AdvancedCameraCardView[] = [
'clips',
'folders',
'gallery',
'snapshots',
'recordings',
'reviews',
];
const FOLDER_VIEW_NAMES: readonly AdvancedCameraCardView[] = ['folder', 'folders'];
const isViewerViewName = (view?: AdvancedCameraCardView): boolean =>
!!view && VIEWER_VIEW_NAMES.includes(view);
const isGalleryViewName = (view?: AdvancedCameraCardView): boolean =>
!!view && GALLERY_VIEW_NAMES.includes(view);
const isAnyFolderViewName = (view?: AdvancedCameraCardView): boolean =>
!!view && FOLDER_VIEW_NAMES.includes(view);
export const isAnyMediaViewName = (view?: AdvancedCameraCardView): boolean =>
isViewerViewName(view) || view === 'live' || view === 'image';
export class View {
public view: AdvancedCameraCardView;
public camera: string | null;
public query: UnifiedQuery | null;
public queryResults: QueryResults | null;
public context: ViewContext | null;
public displayMode: ViewDisplayMode | null;
constructor(params: ViewParameters) {
this.view = params.view;
this.camera = params.camera ?? null;
this.query = params.query ?? null;
this.queryResults = params.queryResults ?? null;
this.context = params.context ?? null;
this.displayMode = params.displayMode ?? null;
}
public clone(): View {
return new View({
view: this.view,
camera: this.camera,
query: this.query?.clone() ?? null,
queryResults: this.queryResults?.clone() ?? null,
context: cloneDeep(this.context),
displayMode: this.displayMode,
});
}
/**
* Evolve this view by changing parameters and returning a new view.
* @param params Parameters to change.
* @returns A new evolved view.
*/
public evolve(params: ViewEvolveParameters): View {
return new View({
view: params.view !== undefined ? params.view : this.view,
camera: params.camera !== undefined ? params.camera : this.camera,
query: params.query !== undefined ? params.query : this.query?.clone() ?? null,
queryResults:
params.queryResults !== undefined
? params.queryResults
: this.queryResults?.clone() ?? null,
context: params.context !== undefined ? params.context : cloneDeep(this.context),
displayMode:
params.displayMode !== undefined ? params.displayMode : this.displayMode,
});
}
/**
* Merge view contexts.
* @param context The context to merge in.
* @returns This view.
*/
public mergeInContext(context?: ViewContext | null): View {
this.context = mergeViewContext(this.context, context);
return this;
}
/**
* Remove a context key.
* @param key The key to remove.
* @returns This view.
*/
public removeContext(key: keyof ViewContext): View {
if (this.context) {
delete this.context[key];
}
return this;
}
public removeContextProperty(
contextKey: keyof ViewContext,
removeKey: PropertyKey,
): View {
const contextObj = this.context?.[contextKey];
if (contextObj) {
delete contextObj[removeKey];
}
return this;
}
/**
* Determine if current view matches a named view.
*/
public is(view: AdvancedCameraCardView): boolean {
return this.view == view;
}
/**
* Determine if a view is a gallery.
*/
public isGalleryView(): boolean {
return isGalleryViewName(this.view);
}
/**
* Determine if a view is of a piece of media (including the media viewer,
* live view, image view -- anything that can create a MediaLoadedInfo event).
*/
public isAnyMediaView(): boolean {
return isAnyMediaViewName(this.view);
}
public isAnyFolderView(): boolean {
return isAnyFolderViewName(this.view);
}
/**
* Determine if a view is for the media viewer.
*/
public isViewerView(): boolean {
return isViewerViewName(this.view);
}
public supportsMultipleDisplayModes(): boolean {
return this.isViewerView() || this.is('live');
}
public getDefaultMediaType(): ViewMediaType | null {
if (['clip', 'clips'].includes(this.view)) {
return 'clips';
}
if (['snapshot', 'snapshots'].includes(this.view)) {
return 'snapshots';
}
if (['recording', 'recordings'].includes(this.view)) {
return 'recordings';
}
if (['review', 'reviews'].includes(this.view)) {
return 'reviews';
}
return null;
}
public isGrid(): boolean {
return this.displayMode === 'grid';
}
}