fix: Show the menu even if the view is not yet initialized (#1541)

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