feat: Make cameras optional (#2343)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user