feat: Make cameras optional (#2343)
This commit is contained in:
@@ -1,123 +0,0 @@
|
||||
import { EffectName, EffectsControllerAPI } 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 EffectsContainer = HTMLElement | DocumentFragment;
|
||||
|
||||
export class EffectsController implements EffectsControllerAPI {
|
||||
private _importedModules: Map<EffectName, EffectModule> = new Map();
|
||||
private _activeInstances: Map<EffectName, EffectComponent | null> = new Map();
|
||||
private _durationTimers: Map<EffectName, Timer> = new Map();
|
||||
private _container: EffectsContainer | null = null;
|
||||
|
||||
public setContainer(container: EffectsContainer | null): void {
|
||||
this._container = container;
|
||||
}
|
||||
|
||||
public async startEffect(name: EffectName, options?: EffectOptions): Promise<void> {
|
||||
if (!this._container || this._activeInstances.has(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reserve the slot immediately with null to prevent concurrent starts.
|
||||
this._activeInstances.set(name, null);
|
||||
|
||||
const effectModule = await this._importEffectModule(name);
|
||||
|
||||
// Check if the effect was cancelled during loading.
|
||||
if (!effectModule || !this._activeInstances.has(name)) {
|
||||
this._activeInstances.delete(name);
|
||||
return;
|
||||
}
|
||||
|
||||
const effectComponent = new effectModule.default();
|
||||
effectComponent.fadeIn = options?.fadeIn ?? true;
|
||||
this._container.appendChild(effectComponent);
|
||||
this._activeInstances.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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async stopEffect(effect: EffectName): Promise<void> {
|
||||
const timer = this._durationTimers.get(effect);
|
||||
if (timer) {
|
||||
timer.stop();
|
||||
this._durationTimers.delete(effect);
|
||||
}
|
||||
|
||||
if (!this._activeInstances.has(effect)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = this._activeInstances.get(effect);
|
||||
this._activeInstances.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._activeInstances.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 effectRegistry[name]?.();
|
||||
if (!effectModule) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this._importedModules.set(name, effectModule);
|
||||
return effectModule;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export type EffectComponent = HTMLElement & {
|
||||
fadeIn: boolean;
|
||||
startFadeOut(): Promise<void>;
|
||||
};
|
||||
|
||||
export type EffectModule = { default: new () => EffectComponent };
|
||||
|
||||
export interface EffectOptions {
|
||||
fadeIn?: boolean;
|
||||
duration?: number;
|
||||
}
|
||||
@@ -31,8 +31,12 @@ import { isBeingCasted } from '../utils/casting';
|
||||
import { getPTZTarget } from '../utils/ptz';
|
||||
import { getStreamCameraID, hasSubstream } from '../utils/substream';
|
||||
import { ViewItemClassifier } from '../view/item-classifier';
|
||||
import { resolveViewName } from '../view/utils/resolve-default';
|
||||
import { View } from '../view/view';
|
||||
import { getCameraIDsForViewName, isViewSupportedByCamera } from '../view/view-support';
|
||||
import {
|
||||
getCameraIDsWithCapabilityForView,
|
||||
isViewSupported,
|
||||
} from '../view/view-support';
|
||||
|
||||
export interface MenuButtonControllerOptions {
|
||||
currentMediaLoadedInfo?: MediaLoadedInfo | null;
|
||||
@@ -112,7 +116,13 @@ export class MenuButtonController {
|
||||
this._getFoldersButton(config, foldersManager, options?.view),
|
||||
|
||||
...this._dynamicMenuButtons.map((button) => ({
|
||||
style: this._getStyleFromActions(config, button, options),
|
||||
style: this._getStyleFromActions(
|
||||
config,
|
||||
cameraManager,
|
||||
foldersManager,
|
||||
button,
|
||||
options,
|
||||
),
|
||||
...button,
|
||||
})),
|
||||
].filter(isTruthy);
|
||||
@@ -175,7 +185,7 @@ export class MenuButtonController {
|
||||
cameraManager: CameraManager,
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
if (!view) {
|
||||
if (!view?.camera) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -184,7 +194,7 @@ export class MenuButtonController {
|
||||
.getAllDependentCameras(view.camera, 'substream');
|
||||
|
||||
if (substreamCameraIDs.size && view.is('live')) {
|
||||
const substreams = [...substreamCameraIDs].filter(
|
||||
const substreams = Array.from(substreamCameraIDs).filter(
|
||||
(cameraID) => cameraID !== view.camera,
|
||||
);
|
||||
const streams = [view.camera, ...substreams];
|
||||
@@ -239,14 +249,13 @@ export class MenuButtonController {
|
||||
foldersManager: FoldersManager,
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
return view &&
|
||||
isViewSupportedByCamera('live', cameraManager, foldersManager, view.camera)
|
||||
return isViewSupported('live', cameraManager, foldersManager, view?.camera)
|
||||
? {
|
||||
icon: 'mdi:cctv',
|
||||
...config.menu.buttons.live,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.view.views.live'),
|
||||
style: view.is('live') ? this._getEmphasizedStyle() : {},
|
||||
style: view?.is('live') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createViewAction('live'),
|
||||
}
|
||||
: null;
|
||||
@@ -258,8 +267,7 @@ export class MenuButtonController {
|
||||
foldersManager: FoldersManager,
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
return view &&
|
||||
isViewSupportedByCamera('clips', cameraManager, foldersManager, view.camera)
|
||||
return isViewSupported('clips', cameraManager, foldersManager, view?.camera)
|
||||
? {
|
||||
icon: 'mdi:filmstrip',
|
||||
...config.menu.buttons.clips,
|
||||
@@ -278,8 +286,7 @@ export class MenuButtonController {
|
||||
foldersManager: FoldersManager,
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
return view &&
|
||||
isViewSupportedByCamera('snapshots', cameraManager, foldersManager, view.camera)
|
||||
return isViewSupported('snapshots', cameraManager, foldersManager, view?.camera)
|
||||
? {
|
||||
icon: 'mdi:camera',
|
||||
...config.menu.buttons.snapshots,
|
||||
@@ -298,14 +305,13 @@ export class MenuButtonController {
|
||||
foldersManager: FoldersManager,
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
return view &&
|
||||
isViewSupportedByCamera('recordings', cameraManager, foldersManager, view.camera)
|
||||
return isViewSupported('recordings', cameraManager, foldersManager, view?.camera)
|
||||
? {
|
||||
icon: 'mdi:album',
|
||||
...config.menu.buttons.recordings,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.view.views.recordings'),
|
||||
style: view.is('recordings') ? this._getEmphasizedStyle() : {},
|
||||
style: view?.is('recordings') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createViewAction('recordings'),
|
||||
hold_action: createViewAction('recording'),
|
||||
}
|
||||
@@ -318,14 +324,13 @@ export class MenuButtonController {
|
||||
foldersManager: FoldersManager,
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
return view &&
|
||||
isViewSupportedByCamera('reviews', cameraManager, foldersManager, view.camera)
|
||||
return isViewSupported('reviews', cameraManager, foldersManager, view?.camera)
|
||||
? {
|
||||
icon: 'mdi:play-box-edit-outline',
|
||||
...config.menu.buttons.reviews,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.view.views.reviews'),
|
||||
style: view.is('reviews') ? this._getEmphasizedStyle() : {},
|
||||
style: view?.is('reviews') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createViewAction('reviews'),
|
||||
hold_action: createViewAction('review'),
|
||||
}
|
||||
@@ -338,14 +343,13 @@ export class MenuButtonController {
|
||||
foldersManager: FoldersManager,
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
return view &&
|
||||
isViewSupportedByCamera('gallery', cameraManager, foldersManager, view.camera)
|
||||
return isViewSupported('gallery', cameraManager, foldersManager, view?.camera)
|
||||
? {
|
||||
icon: 'mdi:play-box-multiple',
|
||||
...config.menu.buttons.gallery,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.menu.buttons.gallery'),
|
||||
style: view.is('gallery') ? this._getEmphasizedStyle() : {},
|
||||
style: view?.is('gallery') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createViewAction('gallery'),
|
||||
hold_action: createViewAction('media'),
|
||||
}
|
||||
@@ -358,8 +362,7 @@ export class MenuButtonController {
|
||||
foldersManager: FoldersManager,
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
return view &&
|
||||
isViewSupportedByCamera('image', cameraManager, foldersManager, view.camera)
|
||||
return isViewSupported('image', cameraManager, foldersManager, view?.camera)
|
||||
? {
|
||||
icon: 'mdi:image',
|
||||
...config.menu.buttons.image,
|
||||
@@ -377,14 +380,13 @@ export class MenuButtonController {
|
||||
foldersManager: FoldersManager,
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
return view &&
|
||||
isViewSupportedByCamera('timeline', cameraManager, foldersManager, view.camera)
|
||||
return isViewSupported('timeline', cameraManager, foldersManager, view?.camera)
|
||||
? {
|
||||
icon: 'mdi:chart-gantt',
|
||||
...config.menu.buttons.timeline,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.view.views.timeline'),
|
||||
style: view.is('timeline') ? this._getEmphasizedStyle() : {},
|
||||
style: view?.is('timeline') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createViewAction('timeline'),
|
||||
}
|
||||
: null;
|
||||
@@ -479,11 +481,12 @@ export class MenuButtonController {
|
||||
view?: View | null,
|
||||
microphoneManager?: MicrophoneManager | null,
|
||||
): MenuItem | null {
|
||||
if (!view) {
|
||||
const streamCameraID = view ? getStreamCameraID(view) : null;
|
||||
if (!streamCameraID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const capabilities = cameraManager.getCameraCapabilities(getStreamCameraID(view));
|
||||
const capabilities = cameraManager.getCameraCapabilities(streamCameraID);
|
||||
|
||||
if (microphoneManager && capabilities?.has('2-way-audio')) {
|
||||
const unavailable =
|
||||
@@ -557,7 +560,9 @@ export class MenuButtonController {
|
||||
if (!view) {
|
||||
return null;
|
||||
}
|
||||
const selectedCameraConfig = cameraManager.getStore().getCameraConfig(view.camera);
|
||||
const selectedCameraConfig = view.camera
|
||||
? cameraManager.getStore().getCameraConfig(view.camera)
|
||||
: null;
|
||||
if (
|
||||
mediaPlayerController?.hasMediaPlayers() &&
|
||||
(view.isViewerView() || (view.is('live') && selectedCameraConfig?.camera_entity))
|
||||
@@ -659,7 +664,7 @@ export class MenuButtonController {
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
const viewCameraIDs = view
|
||||
? getCameraIDsForViewName(view.view, cameraManager, foldersManager)
|
||||
? getCameraIDsWithCapabilityForView(view.view, cameraManager, foldersManager)
|
||||
: null;
|
||||
if (
|
||||
view?.supportsMultipleDisplayModes() &&
|
||||
@@ -754,8 +759,8 @@ export class MenuButtonController {
|
||||
foldersManager?: FoldersManager | null,
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
const folders = [...(foldersManager?.getFolders() ?? [])];
|
||||
if (!folders?.length) {
|
||||
const folders = Array.from(foldersManager?.getFolders() ?? []);
|
||||
if (!foldersManager?.hasFolders()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -823,6 +828,8 @@ export class MenuButtonController {
|
||||
*/
|
||||
protected _getStyleFromActions(
|
||||
config: AdvancedCameraCardConfig,
|
||||
cameraManager: CameraManager,
|
||||
foldersManager: FoldersManager,
|
||||
button: MenuItem,
|
||||
options?: MenuButtonControllerOptions,
|
||||
): StyleInfo {
|
||||
@@ -848,7 +855,9 @@ export class MenuButtonController {
|
||||
),
|
||||
) ||
|
||||
(action.advanced_camera_card_action === 'default' &&
|
||||
options?.view?.is(config.view.default)) ||
|
||||
options?.view?.is(
|
||||
resolveViewName(config.view.default, cameraManager, foldersManager),
|
||||
)) ||
|
||||
(action.advanced_camera_card_action === 'fullscreen' &&
|
||||
!!options?.fullscreenManager?.isInFullscreen()) ||
|
||||
(action.advanced_camera_card_action === 'camera_select' &&
|
||||
|
||||
@@ -458,7 +458,7 @@ export class TimelineController {
|
||||
main: true,
|
||||
},
|
||||
);
|
||||
} else if (panMode === 'seek-in-camera') {
|
||||
} else if (panMode === 'seek-in-camera' && view.camera) {
|
||||
newResults = results
|
||||
.clone()
|
||||
.selectBestResult(
|
||||
|
||||
Reference in New Issue
Block a user