diff --git a/src/card.ts b/src/card.ts index 835bef16..035974df 100644 --- a/src/card.ts +++ b/src/card.ts @@ -258,7 +258,7 @@ class FrigateCard extends LitElement { protected _renderMenu(slot?: string): TemplateResult | void { const view = this._controller.getViewManager().getView(); - if (!this._hass || !this._config || !view) { + if (!this._hass || !this._config) { return; } return html` @@ -271,7 +271,6 @@ class FrigateCard extends LitElement { this._hass, this._config, this._controller.getCameraManager(), - view, { inExpandedMode: this._controller.getExpandManager().isExpanded(), inFullscreenMode: this._controller.getFullscreenManager().isInFullscreen(), @@ -279,6 +278,7 @@ class FrigateCard extends LitElement { showCameraUIButton: this._controller.getCameraURLManager().hasCameraURL(), mediaPlayerController: this._controller.getMediaPlayerManager(), microphoneManager: this._controller.getMicrophoneManager(), + view: view, viewManager: this._controller.getViewManager(), }, )} diff --git a/src/components-lib/menu-button-controller.ts b/src/components-lib/menu-button-controller.ts index 5e1e7fd1..6abf28f9 100644 --- a/src/components-lib/menu-button-controller.ts +++ b/src/components-lib/menu-button-controller.ts @@ -36,6 +36,7 @@ export interface MenuButtonControllerOptions { microphoneManager?: MicrophoneManager | null; mediaPlayerController?: MediaPlayerManager | null; viewManager?: ViewManager | null; + view?: View | null; } export class MenuButtonController { @@ -62,20 +63,19 @@ export class MenuButtonController { hass: HomeAssistant, config: FrigateCardConfig, cameraManager: CameraManager, - view: View, options?: MenuButtonControllerOptions, ): MenuItem[] { return [ this._getFrigateButton(config), - this._getCamerasButton(config, cameraManager, view), - this._getSubstreamsButton(config, cameraManager, view), - this._getLiveButton(config, view, options?.viewManager), - this._getClipsButton(config, view, options?.viewManager), - this._getSnapshotsButton(config, view, options?.viewManager), - this._getRecordingsButton(config, view, options?.viewManager), - this._getImageButton(config, view, options?.viewManager), - this._getTimelineButton(config, view, options?.viewManager), - this._getDownloadButton(config, cameraManager, view), + this._getCamerasButton(config, cameraManager, options?.view), + this._getSubstreamsButton(config, cameraManager, options?.view), + this._getLiveButton(config, options?.view, options?.viewManager), + this._getClipsButton(config, options?.view, options?.viewManager), + this._getSnapshotsButton(config, options?.view, options?.viewManager), + this._getRecordingsButton(config, options?.view, options?.viewManager), + this._getImageButton(config, options?.view, options?.viewManager), + this._getTimelineButton(config, options?.view, options?.viewManager), + this._getDownloadButton(config, cameraManager, options?.view), this._getCameraUIButton(config, options?.showCameraUIButton), this._getMicrophoneButton( config, @@ -88,18 +88,18 @@ export class MenuButtonController { hass, config, cameraManager, - view, + options?.view, options?.mediaPlayerController, ), this._getPlayPauseButton(config, options?.currentMediaLoadedInfo), this._getMuteUnmuteButton(config, options?.currentMediaLoadedInfo), this._getScreenshotButton(config, options?.currentMediaLoadedInfo), - this._getDisplayModeButton(config, cameraManager, view), - this._getPTZControlsButton(config, cameraManager, view), - this._getPTZHomeButton(config, cameraManager, view), + this._getDisplayModeButton(config, cameraManager, options?.view), + this._getPTZControlsButton(config, cameraManager, options?.view), + this._getPTZHomeButton(config, cameraManager, options?.view), ...this._dynamicMenuButtons.map((button) => ({ - style: this._getStyleFromActions(config, view, button, options), + style: this._getStyleFromActions(config, button, options), ...button, })), ].filter(isTruthy); @@ -124,7 +124,7 @@ export class MenuButtonController { protected _getCamerasButton( config: FrigateCardConfig, cameraManager: CameraManager, - view: View, + view?: View | null, ): MenuItem | null { // Show all cameras in the menu rather than just cameras that support the // current view for a less surprising UX. @@ -142,7 +142,7 @@ export class MenuButtonController { entity: config.camera_entity, state_color: true, title: metadata?.title, - selected: view.camera === cameraID, + selected: view?.camera === cameraID, ...(action && { tap_action: action }), }; }, @@ -162,8 +162,12 @@ export class MenuButtonController { protected _getSubstreamsButton( config: FrigateCardConfig, cameraManager: CameraManager, - view: View, + view?: View | null, ): MenuItem | null { + if (!view) { + return null; + } + const substreamCameraIDs = cameraManager .getStore() .getAllDependentCameras(view.camera, 'substream'); @@ -221,10 +225,10 @@ export class MenuButtonController { protected _getLiveButton( config: FrigateCardConfig, - view: View, + view?: View | null, viewManager?: ViewManager | null, ): MenuItem | null { - return viewManager?.isViewSupportedByCamera(view.camera, 'live') + return view && viewManager?.isViewSupportedByCamera(view.camera, 'live') ? { icon: 'mdi:cctv', ...config.menu.buttons.live, @@ -238,10 +242,10 @@ export class MenuButtonController { protected _getClipsButton( config: FrigateCardConfig, - view: View, + view?: View | null, viewManager?: ViewManager | null, ): MenuItem | null { - return viewManager?.isViewSupportedByCamera(view.camera, 'clips') + return view && viewManager?.isViewSupportedByCamera(view.camera, 'clips') ? { icon: 'mdi:filmstrip', ...config.menu.buttons.clips, @@ -256,10 +260,10 @@ export class MenuButtonController { protected _getSnapshotsButton( config: FrigateCardConfig, - view: View, + view?: View | null, viewManager?: ViewManager | null, ): MenuItem | null { - return viewManager?.isViewSupportedByCamera(view.camera, 'snapshots') + return view && viewManager?.isViewSupportedByCamera(view.camera, 'snapshots') ? { icon: 'mdi:camera', ...config.menu.buttons.snapshots, @@ -274,10 +278,10 @@ export class MenuButtonController { protected _getRecordingsButton( config: FrigateCardConfig, - view: View, + view?: View | null, viewManager?: ViewManager | null, ): MenuItem | null { - return viewManager?.isViewSupportedByCamera(view.camera, 'recordings') + return view && viewManager?.isViewSupportedByCamera(view.camera, 'recordings') ? { icon: 'mdi:album', ...config.menu.buttons.recordings, @@ -292,10 +296,10 @@ export class MenuButtonController { protected _getImageButton( config: FrigateCardConfig, - view: View, + view?: View | null, viewManager?: ViewManager | null, ): MenuItem | null { - return viewManager?.isViewSupportedByCamera(view.camera, 'image') + return view && viewManager?.isViewSupportedByCamera(view.camera, 'image') ? { icon: 'mdi:image', ...config.menu.buttons.image, @@ -309,10 +313,10 @@ export class MenuButtonController { protected _getTimelineButton( config: FrigateCardConfig, - view: View, + view?: View | null, viewManager?: ViewManager | null, ): MenuItem | null { - return viewManager?.isViewSupportedByCamera(view.camera, 'timeline') + return view && viewManager?.isViewSupportedByCamera(view.camera, 'timeline') ? { icon: 'mdi:chart-gantt', ...config.menu.buttons.timeline, @@ -327,14 +331,14 @@ export class MenuButtonController { protected _getDownloadButton( config: FrigateCardConfig, cameraManager: CameraManager, - view: View, + view?: View | null, ): MenuItem | null { - const selectedMedia = view.queryResults?.getSelectedResult(); + const selectedMedia = view?.queryResults?.getSelectedResult(); const mediaCapabilities = selectedMedia ? cameraManager?.getMediaCapabilities(selectedMedia) : null; if ( - view.isViewerView() && + view?.isViewerView() && mediaCapabilities?.canDownload && !this._isBeingCasted() ) { @@ -437,9 +441,12 @@ export class MenuButtonController { hass: HomeAssistant, config: FrigateCardConfig, cameraManager: CameraManager, - view: View, + view?: View | null, mediaPlayerController?: MediaPlayerManager | null, ): MenuItem | null { + if (!view) { + return null; + } const selectedCameraConfig = cameraManager.getStore().getCameraConfig(view.camera); if ( mediaPlayerController?.hasMediaPlayers() && @@ -543,10 +550,16 @@ export class MenuButtonController { protected _getDisplayModeButton( config: FrigateCardConfig, cameraManager: CameraManager, - view: View, + view?: View | null, ): MenuItem | null { - const viewCameraIDs = getCameraIDsForViewName(cameraManager, view.view); - if (view.supportsMultipleDisplayModes() && viewCameraIDs.size > 1) { + const viewCameraIDs = view + ? getCameraIDsForViewName(cameraManager, view.view) + : null; + if ( + view?.supportsMultipleDisplayModes() && + viewCameraIDs && + viewCameraIDs.size > 1 + ) { const isGrid = view.isGrid(); return { icon: isGrid ? 'mdi:grid-off' : 'mdi:grid', @@ -565,15 +578,15 @@ export class MenuButtonController { protected _getPTZControlsButton( config: FrigateCardConfig, cameraManager: CameraManager, - view: View, + view?: View | null, ): MenuItem | null { - const ptzConfig = view.is('live') + const ptzConfig = view?.is('live') ? config.live.controls.ptz - : view.isViewerView() + : view?.isViewerView() ? config.media_viewer.controls.ptz : null; - if (!ptzConfig || ptzConfig.mode === 'off') { + if (!view || !ptzConfig || ptzConfig.mode === 'off') { return null; } @@ -602,16 +615,18 @@ export class MenuButtonController { protected _getPTZHomeButton( config: FrigateCardConfig, cameraManager: CameraManager, - view: View, + view?: View | null, ): MenuItem | null { - const target = getPTZTarget(view, { - cameraManager: cameraManager, - }); + const target = view + ? getPTZTarget(view, { + cameraManager: cameraManager, + }) + : null; if ( !target || ((target.type === 'digital' && - view.context?.zoom?.[target.targetID]?.observed?.isDefault) ?? + view?.context?.zoom?.[target.targetID]?.observed?.isDefault) ?? true) ) { return null; @@ -652,7 +667,6 @@ export class MenuButtonController { */ protected _getStyleFromActions( config: FrigateCardConfig, - view: View, button: MenuItem, options?: MenuButtonControllerOptions, ): StyleInfo { @@ -679,14 +693,14 @@ export class MenuButtonController { FRIGATE_CARD_VIEWS_USER_SPECIFIED.some( (viewName) => viewName === frigateCardAction.frigate_card_action && - view?.is(frigateCardAction.frigate_card_action), + options?.view?.is(frigateCardAction.frigate_card_action), ) || (frigateCardAction.frigate_card_action === 'default' && - view.is(config.view.default)) || + options?.view?.is(config.view.default)) || (frigateCardAction.frigate_card_action === 'fullscreen' && !!options?.inFullscreenMode) || (frigateCardAction.frigate_card_action === 'camera_select' && - view.camera === frigateCardAction.camera) + options?.view?.camera === frigateCardAction.camera) ) { return this._getEmphasizedStyle(); } diff --git a/tests/components-lib/menu-button-controller.test.ts b/tests/components-lib/menu-button-controller.test.ts index 5e7d5c30..dc466efb 100644 --- a/tests/components-lib/menu-button-controller.test.ts +++ b/tests/components-lib/menu-button-controller.test.ts @@ -2,6 +2,7 @@ import { HomeAssistant } from '@dermotduffy/custom-card-helpers'; import isEqual from 'lodash-es/isEqual'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; +import { Capabilities } from '../../src/camera-manager/capabilities'; import { CameraManager } from '../../src/camera-manager/manager'; import { CameraManagerCameraMetadata } from '../../src/camera-manager/types'; import { MediaPlayerManager } from '../../src/card-controller/media-player-manager'; @@ -23,9 +24,9 @@ import { ViewMedia } from '../../src/view/media'; import { MediaQueriesResults } from '../../src/view/media-queries-results'; import { View } from '../../src/view/view'; import { - createCapabilities, createCameraConfig, createCameraManager, + createCapabilities, createCardAPI, createConfig, createHASS, @@ -36,7 +37,6 @@ import { createView, TestViewMedia, } from '../test-utils'; -import { Capabilities } from '../../src/camera-manager/capabilities'; vi.mock('../../src/utils/media-player-controller.js'); vi.mock('../../src/card-controller/microphone-manager.js'); @@ -47,7 +47,7 @@ const calculateButtons = ( hass?: HomeAssistant; config?: FrigateCardConfig; cameraManager?: CameraManager; - view?: View; + view?: View | null; viewManager?: ViewManager; }, ): MenuItem[] => { @@ -60,8 +60,11 @@ const calculateButtons = ( options?.hass ?? createHASS(), options?.config ?? createConfig(), cameraManager, - options?.view ?? createView({ camera: 'camera-1' }), - options, + { + ...options, + view: + options?.view === undefined ? createView({ camera: 'camera-1' }) : options.view, + }, ); }; @@ -184,6 +187,19 @@ describe('MenuButtonController', () => { }); describe('should have substream button', () => { + it('with no view', () => { + const buttons = calculateButtons(controller, { + cameraManager: createCameraManager(), + view: null, + }); + + expect(buttons).not.toContainEqual( + expect.objectContaining({ + title: 'Substream(s)', + }), + ); + }); + it('with no dependency', () => { const cameraManager = createCameraManager(); vi.mocked(cameraManager.getStore).mockReturnValue(