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
@@ -8,13 +8,13 @@ export class EffectAction extends AdvancedCameraCardAction<EffectActionConfig> {
switch (this._action.effect_action) {
case 'start':
api.getEffectsControllerAPI()?.startEffect(this._action.effect);
api.getEffectsManager().startEffect(this._action.effect);
break;
case 'stop':
api.getEffectsControllerAPI()?.stopEffect(this._action.effect);
api.getEffectsManager().stopEffect(this._action.effect);
break;
case 'toggle':
api.getEffectsControllerAPI()?.toggleEffect(this._action.effect);
api.getEffectsManager().toggleEffect(this._action.effect);
break;
}
}
@@ -20,7 +20,10 @@ export class MediaPlayerAction extends AdvancedCameraCardAction<MediaPlayerActio
const item = view?.queryResults?.getSelectedResult() ?? null;
if (view?.is('live')) {
await mediaPlayerController.playLive(mediaPlayer, getStreamCameraID(view));
const cameraID = getStreamCameraID(view);
if (cameraID) {
await mediaPlayerController.playLive(mediaPlayer, cameraID);
}
} else if (view?.isViewerView() && item && ViewItemClassifier.isMedia(item)) {
await mediaPlayerController.playMedia(mediaPlayer, item);
}
@@ -30,9 +30,10 @@ export class SetReviewAction extends AdvancedCameraCardAction<SetReviewActionCon
viewManager.getEpoch(),
getReviewedQueryFilterFromQuery(view?.query, item),
),
api
.getEffectsControllerAPI()
?.startEffect('check', { duration: 0.4, fadeIn: false }),
api.getEffectsManager().startEffect('check', {
duration: 0.4,
fadeIn: false,
}),
]);
// Trigger UI update to refresh menu icon state
@@ -47,11 +47,11 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
// When the camera changes, update the entity to match (only if different
// to avoid race conditions when multiple cards share the same entity).
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2244
createInternalCallbackAction((api: CardActionsAPI) =>
createInternalCallbackAction(async (api: CardActionsAPI) =>
selectOptionOnEntityIfDifferent(
cameraControlEntity,
api.getViewManager().getView()?.camera,
api,
cameraControlEntity,
api.getViewManager().getView()?.camera ?? undefined,
),
),
],
@@ -76,13 +76,10 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
`{{ hass.states["${cameraControlEntity}"].state }}`,
)
: // Set the selected option in the entity to the current camera ID.
createInternalCallbackAction((api: CardActionsAPI) =>
selectOptionOnEntityIfDifferent(
cameraControlEntity,
api.getViewManager().getView()?.camera,
api,
),
),
createInternalCallbackAction(async (api: CardActionsAPI) => {
const camera = api.getViewManager().getView()?.camera ?? undefined;
return selectOptionOnEntityIfDifferent(api, cameraControlEntity, camera);
}),
],
tag: automationTag,
},
@@ -108,9 +105,9 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
};
const selectOptionOnEntityIfDifferent = async (
entity: string,
option: string | undefined,
api: CardActionsAPI,
entity: string,
option?: string,
): Promise<void> => {
const hass = api.getHASSManager().getHASS();
const currentState = hass?.states[entity]?.state;
+4 -10
View File
@@ -1,5 +1,6 @@
import { ReactiveController } from 'lit';
import { CameraManager } from '../camera-manager/manager';
import { EffectsManager } from './effects/effects-manager';
import { ConditionStateManager } from '../conditions/state-manager';
import { AdvancedCameraCardConfig } from '../config/schema/types';
import { DeviceRegistryManager } from '../ha/registry/device';
@@ -8,7 +9,6 @@ import { EntityRegistryManagerLive } from '../ha/registry/entity';
import { EntityCache, EntityRegistryManager } from '../ha/registry/entity/types';
import { ResolvedMediaCache } from '../ha/resolved-media';
import { LovelaceCardEditor } from '../ha/types';
import { EffectsControllerAPI } from '../types';
import { ActionsManager } from './actions/actions-manager';
import { AutomationsManager } from './automations-manager';
import { CameraURLManager } from './camera-url-manager';
@@ -28,7 +28,6 @@ import { InitializationManager } from './initialization-manager';
import { InteractionManager } from './interaction-manager';
import { KeyboardStateManager } from './keyboard-state-manager';
import { MediaLoadedInfoManager } from './media-info-manager';
import { MediaPlayerManager } from './media-player-manager';
import { MessageManager } from './message-manager';
import { MicrophoneManager } from './microphone-manager';
@@ -67,8 +66,6 @@ import {
import { ViewItemManager } from './view/item-manager';
import { ViewManager } from './view/view-manager';
type EffectsControllerAPICallback = () => EffectsControllerAPI | null;
export class CardController
implements
CardActionsManagerAPI,
@@ -97,9 +94,8 @@ export class CardController
CardViewAPI,
ReactiveController
{
protected _effectsControllerAPICallback: EffectsControllerAPICallback;
protected _conditionStateManager = new ConditionStateManager();
protected _effectsManager = new EffectsManager();
// These properties may be used in the construction of 'managers' (and should
// be created first).
@@ -138,7 +134,6 @@ export class CardController
host: CardHTMLElement,
scrollCallback: ScrollCallback,
menuToggleCallback: MenuToggleCallback,
effectsControllerAPICallback: EffectsControllerAPICallback,
) {
host.addController(this);
@@ -148,7 +143,6 @@ export class CardController
scrollCallback,
menuToggleCallback,
);
this._effectsControllerAPICallback = effectsControllerAPICallback;
}
// *************************************************************************
@@ -199,8 +193,8 @@ export class CardController
return this._deviceRegistryManager;
}
public getEffectsControllerAPI(): EffectsControllerAPI | null {
return this._effectsControllerAPICallback();
public getEffectsManager(): EffectsManager {
return this._effectsManager;
}
public getEntityRegistryManager(): EntityRegistryManager {
@@ -0,0 +1,172 @@
import { EffectName, EffectsContainer, EffectsManagerInterface } from '../../types';
import { Timer } from '../../utils/timer';
import { EffectComponent, EffectModule, EffectOptions } from './types';
const effectRegistry: Record<EffectName, () => Promise<EffectModule>> = {
check: async () => {
const module = await import('../../components/effects/check');
return { default: module.AdvancedCameraCardEffectCheck };
},
fireworks: async () => {
const module = await import('../../components/effects/fireworks');
return { default: module.AdvancedCameraCardEffectFireworks };
},
ghost: async () => {
const module = await import('../../components/effects/ghost');
return { default: module.AdvancedCameraCardEffectGhost };
},
hearts: async () => {
const module = await import('../../components/effects/hearts');
return { default: module.AdvancedCameraCardEffectHearts };
},
shamrocks: async () => {
const module = await import('../../components/effects/shamrocks');
return { default: module.AdvancedCameraCardEffectShamrocks };
},
snow: async () => {
const module = await import('../../components/effects/snow');
return { default: module.AdvancedCameraCardEffectSnow };
},
};
type EffectModuleImporter = (name: EffectName) => Promise<EffectModule | null>;
const defaultImportEffectModule: EffectModuleImporter = async (name: EffectName) => {
const effectModule = await effectRegistry[name]?.();
return effectModule ?? null;
};
export class EffectsManager implements EffectsManagerInterface {
private _importer: EffectModuleImporter;
private _importedModules: Map<EffectName, EffectModule> = new Map();
private _durationTimers: Map<EffectName, Timer> = new Map();
// Effects that have been requested to start but are still loading or waiting
// for a container to be registered (test case: a card without cameras or
// folders will initialize very quickly and the card will be loaded before
// effects can be started).
private _pendingEffects: Map<EffectName, EffectOptions | undefined> = new Map();
private _activeEffects: Map<EffectName, EffectComponent | null> = new Map();
protected _container: EffectsContainer | null = null;
constructor(importer: EffectModuleImporter = defaultImportEffectModule) {
this._importer = importer;
}
public setContainer(container: EffectsContainer): void {
this._container = container;
this._startPendingEffects();
}
public removeContainer(): void {
this._container = null;
this._pendingEffects.clear();
for (const timer of this._durationTimers.values()) {
timer.stop();
}
this._durationTimers.clear();
for (const instance of this._activeEffects.values()) {
instance?.remove();
}
this._activeEffects.clear();
}
public async startEffect(name: EffectName, options?: EffectOptions): Promise<void> {
if (this._activeEffects.has(name)) {
return;
}
// Reserve the slot immediately with null to prevent concurrent starts.
this._activeEffects.set(name, null);
if (!this._container) {
this._pendingEffects.set(name, options);
return;
}
await this._startEffect(name, options);
}
public async stopEffect(effect: EffectName): Promise<void> {
const timer = this._durationTimers.get(effect);
if (timer) {
timer.stop();
this._durationTimers.delete(effect);
}
this._pendingEffects.delete(effect);
if (!this._activeEffects.has(effect)) {
return;
}
const instance = this._activeEffects.get(effect);
this._activeEffects.delete(effect);
// If instance is null, it's still loading - just clearing the reservation
// will prevent it from appearing (startEffect checks this after import).
if (instance) {
await instance.startFadeOut();
instance.remove();
}
}
public async toggleEffect(name: EffectName, options?: EffectOptions): Promise<void> {
if (this._activeEffects.has(name)) {
await this.stopEffect(name);
} else {
await this.startEffect(name, options);
}
}
private async _importEffectModule(name: EffectName): Promise<EffectModule | null> {
const existingModule = this._importedModules.get(name);
if (existingModule) {
return existingModule;
}
const effectModule = await this._importer(name);
if (!effectModule) {
return null;
}
this._importedModules.set(name, effectModule);
return effectModule;
}
private async _startEffect(name: EffectName, options?: EffectOptions): Promise<void> {
const effectModule = await this._importEffectModule(name);
this._pendingEffects.delete(name);
if (!effectModule || !this._activeEffects.has(name) || !this._container) {
this._activeEffects.delete(name);
return;
}
const effectComponent = new effectModule.default();
effectComponent.fadeIn = options?.fadeIn ?? true;
this._container.appendChild(effectComponent);
this._activeEffects.set(name, effectComponent);
const duration = options?.duration;
if (duration !== undefined) {
return new Promise<void>((resolve) => {
const timer = new Timer();
this._durationTimers.set(name, timer);
timer.start(duration, async () => {
this._durationTimers.delete(name);
await this.stopEffect(name);
resolve();
});
});
}
}
private _startPendingEffects(): void {
for (const [name, options] of this._pendingEffects.entries()) {
this._pendingEffects.delete(name);
this._startEffect(name, options);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
export type EffectComponent = HTMLElement & {
fadeIn: boolean;
startFadeOut(): Promise<void>;
};
export type EffectModule = { default: new () => EffectComponent };
export interface EffectOptions {
fadeIn?: boolean;
duration?: number;
}
+2 -2
View File
@@ -5,12 +5,12 @@ import {
createGeneralAction,
createViewAction,
} from '../utils/action.js';
import { ViewParameters } from '../view/view';
import { CardQueryStringAPI } from './types';
import { SubstreamSelectViewModifier } from './view/modifiers/substream-select';
import { ViewParametersUserSpecified } from './view/types.js';
interface QueryStringViewIntent {
view?: Partial<ViewParameters> & {
view?: ViewParametersUserSpecified & {
default?: boolean;
substream?: string;
};
@@ -43,8 +43,8 @@ export class StatusBarItemManager {
view?: View | null;
mediaLoadedInfo?: MediaLoadedInfo | null;
}): StatusBarItem[] {
const cameraMetadata = options?.view
? options?.cameraManager?.getCameraMetadata(options?.view?.camera)
const cameraMetadata = options?.view?.camera
? options?.cameraManager?.getCameraMetadata(options.view.camera)
: null;
const engineIcon = cameraMetadata?.engineIcon ?? null;
const selectedResult = options?.view?.queryResults?.getSelectedResult();
+2 -2
View File
@@ -4,7 +4,7 @@ import type { Automation } from '../config/schema/automations';
import type { DeviceRegistryManager } from '../ha/registry/device';
import type { EntityRegistryManager } from '../ha/registry/entity/types';
import type { ResolvedMediaCache } from '../ha/resolved-media';
import type { EffectsControllerAPI } from '../types';
import type { EffectsManagerInterface } from '../types';
import type { ActionsManager } from './actions/actions-manager';
import type { AutomationsManager } from './automations-manager';
import type { CameraURLManager } from './camera-url-manager';
@@ -44,7 +44,7 @@ export interface CardActionsAPI {
getCardElementManager(): CardElementManager;
getConditionStateManager(): ConditionStateManager;
getConfigManager(): ConfigManager;
getEffectsControllerAPI(): EffectsControllerAPI | null;
getEffectsManager(): EffectsManagerInterface;
getExpandManager(): ExpandManager;
getFoldersManager(): FoldersManager;
getFullscreenManager(): FullscreenManager;
+198 -118
View File
@@ -1,15 +1,22 @@
import { AdvancedCameraCardView, VIEW_DEFAULT } from '../../config/schema/common/const';
import { AdvancedCameraCardView } from '../../config/schema/common/const';
import { ViewDisplayMode } from '../../config/schema/common/display';
import { AdvancedCameraCardConfig } from '../../config/schema/types';
import { localize } from '../../localize/localize';
import { resolveViewName } from '../../view/utils/resolve-default';
import { View, ViewParameters } from '../../view/view';
import {
getCameraIDsForViewName,
doesViewRequireCamera,
getCameraIDsWithCapabilityForView,
isViewSupportedByCamera,
} from '../../view/view-support';
import { CardViewAPI } from '../types';
import { applyViewModifiers } from './modifiers';
import { ViewFactoryOptions, ViewIncompatible, ViewNoCameraError } from './types';
import { ViewFactoryOptions, ViewIncompatible } from './types';
interface ResolvedViewTarget {
viewName: AdvancedCameraCardView;
cameraID: string | null;
}
export class ViewFactory {
protected _api: CardViewAPI;
@@ -24,130 +31,72 @@ export class ViewFactory {
return null;
}
// Neither options.baseView.camera nor options.baseView.view are respected
// here, since this is the default view / camera.
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1564
let cameraID: string | null = null;
const viewName = options?.params?.view ?? config.view.default;
if (options?.params?.camera) {
cameraID = options.params.camera;
} else {
const cameraIDs = [
...getCameraIDsForViewName(
viewName,
this._api.getCameraManager(),
this._api.getFoldersManager(),
),
];
if (
cameraIDs.length &&
options?.baseView?.camera &&
config.view.default_cycle_camera
) {
const currentIndex = cameraIDs.indexOf(options.baseView.camera);
const targetIndex = currentIndex + 1 >= cameraIDs.length ? 0 : currentIndex + 1;
cameraID = cameraIDs[targetIndex];
} else {
cameraID = cameraIDs[0] ?? null;
}
}
const viewName = this._getDefaultViewName(config);
return this.getViewByParameters({
params: {
...options?.params,
view: viewName,
camera: cameraID,
camera: this._getDefaultCameraID(config, viewName, options),
},
baseView: options?.baseView,
});
}
protected _getDefaultViewName = (
config: AdvancedCameraCardConfig,
): AdvancedCameraCardView =>
resolveViewName(
config.view.default,
this._api.getCameraManager(),
this._api.getFoldersManager(),
);
protected _getDefaultCameraID(
config: AdvancedCameraCardConfig,
viewName: AdvancedCameraCardView,
options?: ViewFactoryOptions,
): string | null {
if (options?.params?.camera) {
return options.params.camera;
}
const cameraIDs = [
...getCameraIDsWithCapabilityForView(
viewName,
this._api.getCameraManager(),
this._api.getFoldersManager(),
),
];
if (
cameraIDs.length &&
options?.baseView?.camera &&
config.view.default_cycle_camera
) {
const currentIndex = cameraIDs.indexOf(options.baseView.camera);
const targetIndex = currentIndex + 1 >= cameraIDs.length ? 0 : currentIndex + 1;
return cameraIDs[targetIndex];
}
return cameraIDs[0] ?? null;
}
public getViewByParameters(options?: ViewFactoryOptions): View | null {
const config = this._api.getConfigManager().getConfig();
if (!config) {
return null;
}
let cameraID: string | null =
options?.params?.camera ?? options?.baseView?.camera ?? null;
let viewName =
options?.params?.view ?? options?.baseView?.view ?? config.view.default;
const allCameraIDs = this._api.getCameraManager().getStore().getCameraIDs();
if (!cameraID || !allCameraIDs.has(cameraID)) {
const viewCameraIDs = getCameraIDsForViewName(
viewName,
this._api.getCameraManager(),
this._api.getFoldersManager(),
);
// Reset to the default camera.
cameraID = viewCameraIDs?.keys().next().value ?? null;
}
if (!cameraID) {
const camerasToCapabilities = [
...this._api.getCameraManager().getStore().getCameras(),
].reduce((acc, [cameraID, camera]) => {
const capabilities = camera.getCapabilities()?.getRawCapabilities();
if (capabilities) {
acc[cameraID] = capabilities;
}
return acc;
}, {});
throw new ViewNoCameraError(localize('error.no_supported_cameras'), {
view: viewName,
cameras_capabilities: camerasToCapabilities,
});
}
if (
!isViewSupportedByCamera(
viewName,
this._api.getCameraManager(),
this._api.getFoldersManager(),
cameraID,
)
) {
if (
options?.failSafe &&
isViewSupportedByCamera(
VIEW_DEFAULT,
this._api.getCameraManager(),
this._api.getFoldersManager(),
cameraID,
)
) {
viewName = VIEW_DEFAULT;
} else {
const capabilities = this._api
.getCameraManager()
.getStore()
.getCamera(cameraID)
?.getCapabilities()
?.getRawCapabilities();
throw new ViewIncompatible(localize('error.no_supported_camera'), {
view: viewName,
camera: cameraID,
...(capabilities && { camera_capabilities: capabilities }),
});
}
}
const configuredDisplayMode = this._getDefaultDisplayModeForView(viewName, config);
const displayMode =
// Prioritize the configured display mode (if present).
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1812
(viewName !== options?.baseView?.view ? configuredDisplayMode : null) ??
options?.params?.displayMode ??
options?.baseView?.displayMode ??
configuredDisplayMode ??
'single';
let viewName = this._resolveViewName(config, options);
let cameraID = this._resolveCameraID(viewName, options);
({ viewName, cameraID } = this._ensureViewCompatibility(
viewName,
cameraID,
config,
options,
));
const displayMode = this._resolveDisplayMode(viewName, config, options);
const viewParameters: ViewParameters = {
...options?.params,
@@ -165,22 +114,153 @@ export class ViewFactory {
return view;
}
protected _getDefaultDisplayModeForView(
protected _resolveViewName(
config: AdvancedCameraCardConfig,
options?: ViewFactoryOptions,
): AdvancedCameraCardView {
if (options?.params?.view !== undefined) {
return resolveViewName(
options.params.view,
this._api.getCameraManager(),
this._api.getFoldersManager(),
);
}
return options?.baseView?.view ?? this._getDefaultViewName(config);
}
protected _resolveCameraID(
viewName: AdvancedCameraCardView,
options?: ViewFactoryOptions,
): string | null {
const cameraID = options?.params?.camera ?? options?.baseView?.camera ?? null;
const allCameraIDs = this._api.getCameraManager().getStore().getCameraIDs();
if (cameraID && allCameraIDs.has(cameraID)) {
return cameraID;
}
const viewCameraIDs = getCameraIDsWithCapabilityForView(
viewName,
this._api.getCameraManager(),
this._api.getFoldersManager(),
);
return viewCameraIDs?.keys().next().value ?? null;
}
protected _ensureViewCompatibility(
viewName: AdvancedCameraCardView,
cameraID: string | null,
config: AdvancedCameraCardConfig,
options?: ViewFactoryOptions,
): ResolvedViewTarget {
if (!cameraID && doesViewRequireCamera(viewName)) {
return this._handleNoCameraForView(viewName, config, options);
}
if (
cameraID &&
!isViewSupportedByCamera(
viewName,
this._api.getCameraManager(),
this._api.getFoldersManager(),
cameraID,
)
) {
return this._handleUnsupportedView(viewName, cameraID, config, options);
}
return { viewName, cameraID };
}
protected _handleNoCameraForView(
viewName: AdvancedCameraCardView,
config: AdvancedCameraCardConfig,
options?: ViewFactoryOptions,
): ResolvedViewTarget {
const defaultViewName = this._getDefaultViewName(config);
if (options?.failSafe && !doesViewRequireCamera(defaultViewName)) {
return { viewName: defaultViewName, cameraID: null };
}
if (options?.failSafe) {
return {
viewName: defaultViewName,
cameraID: this._api.getCameraManager().getStore().getDefaultCameraID(),
};
}
throw new ViewIncompatible(localize('error.no_supported_cameras'), {
view: viewName,
camera: null,
default_view: defaultViewName,
});
}
protected _handleUnsupportedView(
viewName: AdvancedCameraCardView,
cameraID: string,
config: AdvancedCameraCardConfig,
options?: ViewFactoryOptions,
): ResolvedViewTarget {
const defaultViewName = this._getDefaultViewName(config);
if (
options?.failSafe &&
isViewSupportedByCamera(
defaultViewName,
this._api.getCameraManager(),
this._api.getFoldersManager(),
cameraID,
)
) {
return { viewName: defaultViewName, cameraID };
}
const capabilities = this._api
.getCameraManager()
.getStore()
.getCamera(cameraID)
?.getCapabilities()
?.getRawCapabilities();
throw new ViewIncompatible(localize('error.no_supported_camera'), {
view: viewName,
camera: cameraID,
default_view: defaultViewName,
...(capabilities && { camera_capabilities: capabilities }),
});
}
protected _resolveDisplayMode(
viewName: AdvancedCameraCardView,
config: AdvancedCameraCardConfig,
options?: ViewFactoryOptions,
): ViewDisplayMode {
const configuredDisplayMode = this._getConfiguredDisplayMode(viewName, config);
// Prioritize the configured display mode (if present).
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1812
return (
(viewName !== options?.baseView?.view ? configuredDisplayMode : null) ??
options?.params?.displayMode ??
options?.baseView?.displayMode ??
configuredDisplayMode ??
'single'
);
}
protected _getConfiguredDisplayMode(
viewName: AdvancedCameraCardView,
config: AdvancedCameraCardConfig,
): ViewDisplayMode | null {
let mode: ViewDisplayMode | null = null;
switch (viewName) {
case 'media':
case 'clip':
case 'recording':
case 'snapshot':
mode = config.media_viewer.display?.mode ?? null;
break;
return config.media_viewer.display?.mode ?? null;
case 'live':
mode = config.live.display?.mode ?? null;
break;
return config.live.display?.mode ?? null;
default:
return null;
}
return mode;
}
}
@@ -15,6 +15,10 @@ export class SubstreamOnViewModifier implements ViewModifier {
}
public modify(view: View): void {
if (!view.camera) {
return;
}
const dependencies = [
...this._api
.getCameraManager()
@@ -27,6 +31,13 @@ export class SubstreamOnViewModifier implements ViewModifier {
}
const currentOverride = getStreamCameraID(view);
/* istanbul ignore if: the if path cannot be reached, as there is a
view.camera guard at the start of this method and getStreamCameraID will
always return non-null as long as camera is present -- @preserve */
if (!currentOverride) {
return;
}
const currentIndex = dependencies.indexOf(currentOverride);
const newIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % dependencies.length;
+6 -2
View File
@@ -3,6 +3,7 @@ import { AdvancedCameraCardError } from '../../types.js';
import { ViewItem } from '../../view/item.js';
import { QueryResults } from '../../view/query-results.js';
import { AdvancedCameraCardUserSpecifiedView } from '../../config/schema/common/const.js';
import { View, ViewParameters } from '../../view/view.js';
export interface ViewModifier {
@@ -26,12 +27,16 @@ export interface QueryExecutorOptions {
useCache?: boolean;
}
export type ViewParametersUserSpecified = Partial<Omit<ViewParameters, 'view'>> & {
view?: AdvancedCameraCardUserSpecifiedView;
};
export interface ViewFactoryOptions {
// An existing view to evolve from.
baseView?: View | null;
// View parameters to set/evolve.
params?: Partial<ViewParameters>;
params?: ViewParametersUserSpecified;
// Modifiers to the view once created.
modifiers?: ViewModifier[];
@@ -70,5 +75,4 @@ export interface ViewManagerInterface {
hasMajorMediaChange(oldView?: View | null, newView?: View | null): boolean;
}
export class ViewNoCameraError extends AdvancedCameraCardError {}
export class ViewIncompatible extends AdvancedCameraCardError {}
+1 -1
View File
@@ -317,7 +317,7 @@ export class ViewManager implements ViewManagerInterface {
this._api.getConditionStateManager()?.setState({
view: view?.view,
camera: view?.camera,
camera: view?.camera ?? undefined,
displayMode: view?.displayMode ?? undefined,
});
@@ -6,6 +6,7 @@ import { UnifiedQuery } from '../../view/unified-query';
import { UnifiedQueryBuilder } from '../../view/unified-query-builder';
import { UnifiedQueryRunner } from '../../view/unified-query-runner';
import { View } from '../../view/view';
import { doesViewRequireCamera, isViewSupported } from '../../view/view-support';
import { CardViewAPI } from '../types';
import { MergeContextViewModifier } from './modifiers/merge-context';
import { RemoveContextPropertyViewModifier } from './modifiers/remove-context-property';
@@ -103,7 +104,26 @@ export class ViewQueryExecutor {
];
};
const cameraForQuery = view.isGrid() ? undefined : view.camera;
// Don't query if the view is not supported (e.g. a camera-based view with
// no cameras).
if (
!isViewSupported(
view.view,
this._api.getCameraManager(),
this._api.getFoldersManager(),
view.camera,
)
) {
return null;
}
// Don't query if the view requires a camera, if it's not in grid mode, but
// no camera is provided.
if (!view.isGrid() && !view.camera && doesViewRequireCamera(view.view)) {
return null;
}
const cameraForQuery = view.isGrid() ? undefined : view.camera ?? undefined;
const getDefaultQueryModifiers = async () => {
const query = builder.buildDefaultCameraQuery(cameraForQuery, {