feat: Make cameras optional (#2343)

This commit is contained in:
Dermot Duffy
2026-02-13 20:21:05 -08:00
committed by GitHub
parent 21f9469784
commit 93f1f08e51
61 changed files with 1312 additions and 557 deletions
+28
View File
@@ -0,0 +1,28 @@
import { CameraManager } from '../../camera-manager/manager';
import { FoldersManager } from '../../card-controller/folders/manager';
import {
AdvancedCameraCardUserSpecifiedView,
AdvancedCameraCardView,
} from '../../config/schema/common/const';
/**
* Resolve a view name that may be 'auto'.
* @param viewName The view name.
* @param cameraManager The camera manager.
* @param foldersManager The folders manager.
* @returns A concrete view name.
*/
export const resolveViewName = (
viewName: AdvancedCameraCardUserSpecifiedView,
cameraManager: CameraManager,
foldersManager: FoldersManager,
): AdvancedCameraCardView => {
if (viewName !== 'auto') {
return viewName;
}
return cameraManager.getStore().getCameraIDs().size
? 'live'
: foldersManager.hasFolders()
? 'folders'
: 'image';
};
+125 -51
View File
@@ -1,64 +1,120 @@
import { CameraManager } from '../camera-manager/manager';
import { CapabilitySearchOptions } from '../camera-manager/types';
import { CapabilitySearchKeys, CapabilitySearchOptions } from '../camera-manager/types';
import { FoldersManager } from '../card-controller/folders/manager';
import { AdvancedCameraCardView } from '../config/schema/common/const';
/**
* Get cameraIDs that are relevant for a given view name based on camera
* capability (if camera specified).
*/
export const getCameraIDsForViewName = (
type ViewSource = 'camera' | 'folder' | 'any';
interface ViewCapabilityRequirements {
src?: ViewSource;
mediaCapabilities?: CapabilitySearchKeys;
mediaCapabilitiesInclusive?: boolean;
}
const anyMedia: ViewCapabilityRequirements = {
src: 'any',
mediaCapabilities: {
anyCapabilities: ['clips', 'snapshots', 'recordings', 'reviews'],
},
mediaCapabilitiesInclusive: false,
};
const anyFolder = { src: 'folder' as const };
const noRequirements = {};
const generateCameraRequirements = (
mediaCapabilities: CapabilitySearchKeys,
mediaCapabilitiesInclusive = true,
): ViewCapabilityRequirements => {
return {
src: 'camera',
mediaCapabilities,
mediaCapabilitiesInclusive,
};
};
const VIEW_REQUIREMENTS: Record<AdvancedCameraCardView, ViewCapabilityRequirements> = {
live: generateCameraRequirements('live', false),
clip: generateCameraRequirements('clips'),
clips: generateCameraRequirements('clips'),
snapshot: generateCameraRequirements('snapshots'),
snapshots: generateCameraRequirements('snapshots'),
recording: generateCameraRequirements('recordings'),
recordings: generateCameraRequirements('recordings'),
review: generateCameraRequirements('reviews'),
reviews: generateCameraRequirements('reviews'),
gallery: anyMedia,
media: anyMedia,
timeline: anyMedia,
folder: anyFolder,
folders: anyFolder,
image: noRequirements,
diagnostics: noRequirements,
};
export const isViewAvailable = (
view: AdvancedCameraCardView,
cameraManager: CameraManager,
foldersManager: FoldersManager,
): boolean => {
const req = VIEW_REQUIREMENTS[view];
const hasCameras = cameraManager.getStore().getCameraIDs().size > 0;
const hasFolders = foldersManager.hasFolders();
if (req.src === 'camera') {
return hasCameras;
}
if (req.src === 'folder') {
return hasFolders;
}
if (req.src === 'any') {
return hasCameras || hasFolders;
}
return true;
};
export const doesViewRequireCamera = (view: AdvancedCameraCardView): boolean => {
return VIEW_REQUIREMENTS[view].src === 'camera';
};
export const getCameraIDsWithCapabilityForView = (
viewName: AdvancedCameraCardView,
cameraManager: CameraManager,
foldersManager: FoldersManager,
cameraID?: string,
): Set<string> => {
const folder = foldersManager.getFolder();
const requirements = VIEW_REQUIREMENTS[viewName];
const allCameras = cameraManager.getStore().getCameraIDs();
switch (viewName) {
case 'diagnostics':
case 'image':
return cameraManager.getStore().getCameraIDs();
case 'folder':
case 'folders':
return folder ? cameraManager.getStore().getCameraIDs() : new Set();
case 'live':
case 'clip':
case 'clips':
case 'snapshot':
case 'snapshots':
case 'recording':
case 'recordings':
case 'review':
case 'reviews':
const options: CapabilitySearchOptions = {
inclusive: viewName !== 'live',
};
const capability =
viewName === 'clip'
? 'clips'
: viewName === 'snapshot'
? 'snapshots'
: viewName === 'recording'
? 'recordings'
: viewName === 'review'
? 'reviews'
: viewName;
return cameraID
? cameraManager.getStore().getAllDependentCameras(cameraID, capability, options)
: cameraManager.getStore().getCameraIDsWithCapability(capability, options);
case 'gallery':
case 'media':
case 'timeline':
return folder
? cameraManager.getStore().getCameraIDs()
: cameraManager.getStore().getCameraIDsWithCapability({
anyCapabilities: ['clips', 'snapshots', 'recordings', 'reviews'],
});
if (requirements.src !== 'camera' && requirements.src !== 'any') {
if (requirements.src === 'folder' && !foldersManager.hasFolders()) {
return new Set();
}
return allCameras;
}
if (requirements.src === 'any' && foldersManager.hasFolders()) {
return allCameras;
}
const options: CapabilitySearchOptions = {
inclusive: !!requirements.mediaCapabilitiesInclusive,
};
const capability = requirements.mediaCapabilities;
/* istanbul ignore next: this path is currently unreachable given the mapping
in VIEW_REQUIREMENTS includes mediaCapabilities for all camera or 'any'
related views -- @preserve */
if (!capability) {
return allCameras;
}
return cameraID
? cameraManager.getStore().getAllDependentCameras(cameraID, capability, options)
: cameraManager.getStore().getCameraIDsWithCapability(capability, options);
};
export const isViewSupportedByCamera = (
@@ -67,5 +123,23 @@ export const isViewSupportedByCamera = (
foldersManager: FoldersManager,
cameraID: string,
): boolean => {
return !!getCameraIDsForViewName(view, cameraManager, foldersManager, cameraID)?.size;
return !!getCameraIDsWithCapabilityForView(
view,
cameraManager,
foldersManager,
cameraID,
)?.size;
};
export const isViewSupported = (
viewName: AdvancedCameraCardView,
cameraManager: CameraManager,
foldersManager: FoldersManager,
cameraID?: string | null,
): boolean => {
return (
isViewAvailable(viewName, cameraManager, foldersManager) &&
(!cameraID ||
isViewSupportedByCamera(viewName, cameraManager, foldersManager, cameraID))
);
};
+3 -4
View File
@@ -16,7 +16,7 @@ declare module 'view' {
interface ViewEvolveParameters {
view?: AdvancedCameraCardView;
camera?: string;
camera?: string | null;
query?: UnifiedQuery | null;
queryResults?: QueryResults | null;
context?: ViewContext | null;
@@ -25,7 +25,6 @@ interface ViewEvolveParameters {
export interface ViewParameters extends ViewEvolveParameters {
view: AdvancedCameraCardView;
camera: string;
}
export const mergeViewContext = (
@@ -37,7 +36,7 @@ export const mergeViewContext = (
export class View {
public view: AdvancedCameraCardView;
public camera: string;
public camera: string | null;
public query: UnifiedQuery | null;
public queryResults: QueryResults | null;
public context: ViewContext | null;
@@ -45,7 +44,7 @@ export class View {
constructor(params: ViewParameters) {
this.view = params.view;
this.camera = params.camera;
this.camera = params.camera ?? null;
this.query = params.query ?? null;
this.queryResults = params.queryResults ?? null;
this.context = params.context ?? null;