Merge pull request #1428 from dermotduffy/menu-refactor

Refactor menu buttons controller into different functions
This commit is contained in:
Dermot Duffy
2024-04-04 23:12:59 +01:00
committed by GitHub
2 changed files with 445 additions and 212 deletions
+269 -113
View File
@@ -5,9 +5,9 @@ import { MediaPlayerManager } from '../card-controller/media-player-manager';
import { MicrophoneManager } from '../card-controller/microphone-manager'; import { MicrophoneManager } from '../card-controller/microphone-manager';
import { ViewManager } from '../card-controller/view-manager'; import { ViewManager } from '../card-controller/view-manager';
import { import {
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
FrigateCardConfig, FrigateCardConfig,
FrigateCardCustomAction, FrigateCardCustomAction,
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
MenuItem, MenuItem,
} from '../config/types'; } from '../config/types';
import { FRIGATE_BUTTON_MENU_ICON } from '../const'; import { FRIGATE_BUTTON_MENU_ICON } from '../const';
@@ -20,6 +20,7 @@ import {
createFrigateCardShowPTZAction, createFrigateCardShowPTZAction,
createFrigateCardSimpleAction, createFrigateCardSimpleAction,
} from '../utils/action'; } from '../utils/action';
import { isTruthy } from '../utils/basic';
import { getEntityIcon, getEntityTitle } from '../utils/ha'; import { getEntityIcon, getEntityTitle } from '../utils/ha';
import { hasUsablePTZ } from '../utils/ptz'; import { hasUsablePTZ } from '../utils/ptz';
import { hasSubstream } from '../utils/substream'; import { hasSubstream } from '../utils/substream';
@@ -63,23 +64,47 @@ export class MenuButtonController {
view: View, view: View,
options?: MenuButtonControllerOptions, options?: MenuButtonControllerOptions,
): MenuItem[] { ): MenuItem[] {
const selectedCameraID = view.camera; return [
const substreamAwareCameraID = this._getFrigateButton(config),
view.context?.live?.overrides?.get(selectedCameraID) ?? selectedCameraID; this._getCamerasButton(config, cameraManager, view),
const selectedCameraConfig = cameraManager this._getSubstreamsButton(config, cameraManager, view),
.getStore() this._getLiveButton(config, view, options?.viewManager),
.getCameraConfig(selectedCameraID); 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._getCameraUIButton(config, options?.showCameraUIButton),
this._getMicrophoneButton(
config,
options?.microphoneManager,
options?.currentMediaLoadedInfo,
),
this._getExpandButton(config, options?.inExpandedMode),
this._getFullscreenButton(config, options?.inFullscreenMode),
this._getCastButton(
hass,
config,
cameraManager,
view,
options?.mediaPlayerController,
),
this._getPlayPauseButton(config, options?.currentMediaLoadedInfo),
this._getMuteUnmuteButton(config, options?.currentMediaLoadedInfo),
this._getScreenshotButton(config, options?.currentMediaLoadedInfo),
this._getDisplayModeButton(config, cameraManager, view),
this._getPTZButton(config, cameraManager, view),
const substreamAwareCameraCapabilities = ...this._dynamicMenuButtons.map((button) => ({
cameraManager.getCameraCapabilities(substreamAwareCameraID); style: this._getStyleFromActions(config, view, button, options),
...button,
})),
].filter(isTruthy);
}
const selectedMedia = view.queryResults?.getSelectedResult(); protected _getFrigateButton(config: FrigateCardConfig): MenuItem {
const mediaCapabilities = selectedMedia return {
? cameraManager?.getMediaCapabilities(selectedMedia)
: null;
const buttons: MenuItem[] = [];
buttons.push({
// Use a magic icon value that the menu will use to render the custom // Use a magic icon value that the menu will use to render the custom
// Frigate icon. // Frigate icon.
icon: FRIGATE_BUTTON_MENU_ICON, icon: FRIGATE_BUTTON_MENU_ICON,
@@ -93,8 +118,14 @@ export class MenuButtonController {
hold_action: createFrigateCardSimpleAction( hold_action: createFrigateCardSimpleAction(
'diagnostics', 'diagnostics',
) as FrigateCardCustomAction, ) as FrigateCardCustomAction,
}); };
}
protected _getCamerasButton(
config: FrigateCardConfig,
cameraManager: CameraManager,
view: View,
): 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.
const menuCameraIDs = cameraManager.getStore().getCameraIDsWithCapability('menu'); const menuCameraIDs = cameraManager.getStore().getCameraIDsWithCapability('menu');
@@ -111,47 +142,53 @@ export class MenuButtonController {
entity: config.camera_entity, entity: config.camera_entity,
state_color: true, state_color: true,
title: metadata?.title, title: metadata?.title,
selected: selectedCameraID === cameraID, selected: view.camera === cameraID,
...(action && { tap_action: action }), ...(action && { tap_action: action }),
}; };
}, },
); );
buttons.push({ return {
icon: 'mdi:video-switch', icon: 'mdi:video-switch',
...config.menu.buttons.cameras, ...config.menu.buttons.cameras,
type: 'custom:frigate-card-menu-submenu', type: 'custom:frigate-card-menu-submenu',
title: localize('config.menu.buttons.cameras'), title: localize('config.menu.buttons.cameras'),
items: menuItems, items: menuItems,
}); };
}
return null;
} }
protected _getSubstreamsButton(
config: FrigateCardConfig,
cameraManager: CameraManager,
view: View,
): MenuItem | null {
const substreamCameraIDs = cameraManager const substreamCameraIDs = cameraManager
.getStore() .getStore()
.getAllDependentCameras(selectedCameraID, 'substream'); .getAllDependentCameras(view.camera, 'substream');
if (selectedCameraID && substreamCameraIDs && view.is('live')) { if (substreamCameraIDs.size && view.is('live')) {
const substreams = [...substreamCameraIDs].filter( const substreams = [...substreamCameraIDs].filter(
(cameraID) => cameraID !== selectedCameraID, (cameraID) => cameraID !== view.camera,
); );
const streams = [selectedCameraID, ...substreams]; const streams = [view.camera, ...substreams];
const substreamAwareCameraID = this._getSubstreamAwareCameraID(view);
if (streams.length === 2) { if (streams.length === 2) {
// If there are only two dependencies (the main camera, and 1 other) // If there are only two dependencies (the main camera, and 1 other)
// then use a button not a menu to toggle. // then use a button not a menu to toggle.
buttons.push({ return {
icon: 'mdi:video-input-component', icon: 'mdi:video-input-component',
style: style:
substreamAwareCameraID !== selectedCameraID substreamAwareCameraID !== view.camera ? this._getEmphasizedStyle() : {},
? this._getEmphasizedStyle()
: {},
title: localize('config.menu.buttons.substreams'), title: localize('config.menu.buttons.substreams'),
...config.menu.buttons.substreams, ...config.menu.buttons.substreams,
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
tap_action: createFrigateCardSimpleAction( tap_action: createFrigateCardSimpleAction(
hasSubstream(view) ? 'live_substream_off' : 'live_substream_on', hasSubstream(view) ? 'live_substream_off' : 'live_substream_on',
) as FrigateCardCustomAction, ) as FrigateCardCustomAction,
}); };
} else if (streams.length > 2) { } else if (streams.length > 2) {
const menuItems = Array.from(streams, (streamID) => { const menuItems = Array.from(streams, (streamID) => {
const action = createFrigateCardCameraAction( const action = createFrigateCardCameraAction(
@@ -166,38 +203,49 @@ export class MenuButtonController {
entity: cameraConfig?.camera_entity, entity: cameraConfig?.camera_entity,
state_color: true, state_color: true,
title: metadata?.title, title: metadata?.title,
selected: selected: substreamAwareCameraID === streamID,
(view.context?.live?.overrides?.get(selectedCameraID) ??
selectedCameraID) === streamID,
...(action && { tap_action: action }), ...(action && { tap_action: action }),
}; };
}); });
buttons.push({ return {
icon: 'mdi:video-input-component', icon: 'mdi:video-input-component',
title: localize('config.menu.buttons.substreams'), title: localize('config.menu.buttons.substreams'),
style: style:
substreamAwareCameraID !== selectedCameraID substreamAwareCameraID !== view.camera ? this._getEmphasizedStyle() : {},
? this._getEmphasizedStyle()
: {},
...config.menu.buttons.substreams, ...config.menu.buttons.substreams,
type: 'custom:frigate-card-menu-submenu', type: 'custom:frigate-card-menu-submenu',
items: menuItems, items: menuItems,
}); };
} }
} }
return null;
}
buttons.push({ protected _getLiveButton(
config: FrigateCardConfig,
view: View,
viewManager?: ViewManager | null,
): MenuItem | null {
return viewManager?.isViewSupportedByCamera(view.camera, 'live')
? {
icon: 'mdi:cctv', icon: 'mdi:cctv',
...config.menu.buttons.live, ...config.menu.buttons.live,
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.live'), title: localize('config.view.views.live'),
style: view.is('live') ? this._getEmphasizedStyle() : {}, style: view.is('live') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardSimpleAction('live') as FrigateCardCustomAction, tap_action: createFrigateCardSimpleAction('live') as FrigateCardCustomAction,
}); }
: null;
}
if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'clips')) { protected _getClipsButton(
buttons.push({ config: FrigateCardConfig,
view: View,
viewManager?: ViewManager | null,
): MenuItem | null {
return viewManager?.isViewSupportedByCamera(view.camera, 'clips')
? {
icon: 'mdi:filmstrip', icon: 'mdi:filmstrip',
...config.menu.buttons.clips, ...config.menu.buttons.clips,
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
@@ -205,11 +253,17 @@ export class MenuButtonController {
style: view?.is('clips') ? this._getEmphasizedStyle() : {}, style: view?.is('clips') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardSimpleAction('clips') as FrigateCardCustomAction, tap_action: createFrigateCardSimpleAction('clips') as FrigateCardCustomAction,
hold_action: createFrigateCardSimpleAction('clip') as FrigateCardCustomAction, hold_action: createFrigateCardSimpleAction('clip') as FrigateCardCustomAction,
}); }
: null;
} }
if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'snapshots')) { protected _getSnapshotsButton(
buttons.push({ config: FrigateCardConfig,
view: View,
viewManager?: ViewManager | null,
): MenuItem | null {
return viewManager?.isViewSupportedByCamera(view.camera, 'snapshots')
? {
icon: 'mdi:camera', icon: 'mdi:camera',
...config.menu.buttons.snapshots, ...config.menu.buttons.snapshots,
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
@@ -221,11 +275,17 @@ export class MenuButtonController {
hold_action: createFrigateCardSimpleAction( hold_action: createFrigateCardSimpleAction(
'snapshot', 'snapshot',
) as FrigateCardCustomAction, ) as FrigateCardCustomAction,
}); }
: null;
} }
if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'recordings')) { protected _getRecordingsButton(
buttons.push({ config: FrigateCardConfig,
view: View,
viewManager?: ViewManager | null,
): MenuItem | null {
return viewManager?.isViewSupportedByCamera(view.camera, 'recordings')
? {
icon: 'mdi:album', icon: 'mdi:album',
...config.menu.buttons.recordings, ...config.menu.buttons.recordings,
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
@@ -237,45 +297,77 @@ export class MenuButtonController {
hold_action: createFrigateCardSimpleAction( hold_action: createFrigateCardSimpleAction(
'recording', 'recording',
) as FrigateCardCustomAction, ) as FrigateCardCustomAction,
}); }
: null;
} }
buttons.push({ protected _getImageButton(
config: FrigateCardConfig,
view: View,
viewManager?: ViewManager | null,
): MenuItem | null {
return viewManager?.isViewSupportedByCamera(view.camera, 'image')
? {
icon: 'mdi:image', icon: 'mdi:image',
...config.menu.buttons.image, ...config.menu.buttons.image,
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.image'), title: localize('config.view.views.image'),
style: view?.is('image') ? this._getEmphasizedStyle() : {}, style: view?.is('image') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardSimpleAction('image') as FrigateCardCustomAction, tap_action: createFrigateCardSimpleAction('image') as FrigateCardCustomAction,
}); }
: null;
}
if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'timeline')) { protected _getTimelineButton(
buttons.push({ config: FrigateCardConfig,
view: View,
viewManager?: ViewManager | null,
): MenuItem | null {
return viewManager?.isViewSupportedByCamera(view.camera, 'timeline')
? {
icon: 'mdi:chart-gantt', icon: 'mdi:chart-gantt',
...config.menu.buttons.timeline, ...config.menu.buttons.timeline,
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
title: localize('config.view.views.timeline'), title: localize('config.view.views.timeline'),
style: view.is('timeline') ? this._getEmphasizedStyle() : {}, style: view.is('timeline') ? this._getEmphasizedStyle() : {},
tap_action: createFrigateCardSimpleAction('timeline') as FrigateCardCustomAction, tap_action: createFrigateCardSimpleAction(
}); 'timeline',
) as FrigateCardCustomAction,
}
: null;
} }
protected _getDownloadButton(
config: FrigateCardConfig,
cameraManager: CameraManager,
view: View,
): MenuItem | null {
const selectedMedia = view.queryResults?.getSelectedResult();
const mediaCapabilities = selectedMedia
? cameraManager?.getMediaCapabilities(selectedMedia)
: null;
if ( if (
view.isViewerView() && view.isViewerView() &&
mediaCapabilities?.canDownload && mediaCapabilities?.canDownload &&
!this._isBeingCasted() !this._isBeingCasted()
) { ) {
buttons.push({ return {
icon: 'mdi:download', icon: 'mdi:download',
...config.menu.buttons.download, ...config.menu.buttons.download,
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.download'), title: localize('config.menu.buttons.download'),
tap_action: createFrigateCardSimpleAction('download') as FrigateCardCustomAction, tap_action: createFrigateCardSimpleAction('download') as FrigateCardCustomAction,
}); };
}
return null;
} }
if (options?.showCameraUIButton) { protected _getCameraUIButton(
buttons.push({ config: FrigateCardConfig,
showCameraUIButton?: boolean,
): MenuItem | null {
return showCameraUIButton
? {
icon: 'mdi:web', icon: 'mdi:web',
...config.menu.buttons.camera_ui, ...config.menu.buttons.camera_ui,
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
@@ -283,17 +375,20 @@ export class MenuButtonController {
tap_action: createFrigateCardSimpleAction( tap_action: createFrigateCardSimpleAction(
'camera_ui', 'camera_ui',
) as FrigateCardCustomAction, ) as FrigateCardCustomAction,
}); }
: null;
} }
if ( protected _getMicrophoneButton(
options?.microphoneManager && config: FrigateCardConfig,
options?.currentMediaLoadedInfo?.capabilities?.supports2WayAudio microphoneManager?: MicrophoneManager | null,
) { currentMediaLoadedInfo?: MediaLoadedInfo | null,
const forbidden = options.microphoneManager.isForbidden(); ): MenuItem | null {
const muted = options.microphoneManager.isMuted(); if (microphoneManager && currentMediaLoadedInfo?.capabilities?.supports2WayAudio) {
const forbidden = microphoneManager.isForbidden();
const muted = microphoneManager.isMuted();
const buttonType = config.menu.buttons.microphone.type; const buttonType = config.menu.buttons.microphone.type;
buttons.push({ return {
icon: forbidden icon: forbidden
? 'mdi:microphone-message-off' ? 'mdi:microphone-message-off'
: muted : muted
@@ -315,41 +410,59 @@ export class MenuButtonController {
...(!forbidden && ...(!forbidden &&
buttonType === 'toggle' && { buttonType === 'toggle' && {
tap_action: createFrigateCardSimpleAction( tap_action: createFrigateCardSimpleAction(
options.microphoneManager.isMuted() muted ? 'microphone_unmute' : 'microphone_mute',
? 'microphone_unmute'
: 'microphone_mute',
) as FrigateCardCustomAction, ) as FrigateCardCustomAction,
}), }),
}); };
}
return null;
} }
if (!this._isBeingCasted()) { protected _getExpandButton(
buttons.push({ config: FrigateCardConfig,
icon: options?.inFullscreenMode ? 'mdi:fullscreen-exit' : 'mdi:fullscreen', inExpandedMode?: boolean,
): MenuItem {
return {
icon: inExpandedMode ? 'mdi:arrow-collapse-all' : 'mdi:arrow-expand-all',
...config.menu.buttons.expand,
type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.expand'),
tap_action: createFrigateCardSimpleAction('expand') as FrigateCardCustomAction,
style: inExpandedMode ? this._getEmphasizedStyle() : {},
};
}
protected _getFullscreenButton(
config: FrigateCardConfig,
inFullscreenMode?: boolean,
): MenuItem | null {
return !this._isBeingCasted()
? {
icon: inFullscreenMode ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
...config.menu.buttons.fullscreen, ...config.menu.buttons.fullscreen,
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.fullscreen'), title: localize('config.menu.buttons.fullscreen'),
tap_action: createFrigateCardSimpleAction( tap_action: createFrigateCardSimpleAction(
'fullscreen', 'fullscreen',
) as FrigateCardCustomAction, ) as FrigateCardCustomAction,
style: options?.inFullscreenMode ? this._getEmphasizedStyle() : {}, style: inFullscreenMode ? this._getEmphasizedStyle() : {},
}); }
: null;
} }
buttons.push({ protected _getCastButton(
icon: options?.inExpandedMode ? 'mdi:arrow-collapse-all' : 'mdi:arrow-expand-all', hass: HomeAssistant,
...config.menu.buttons.expand, config: FrigateCardConfig,
type: 'custom:frigate-card-menu-icon', cameraManager: CameraManager,
title: localize('config.menu.buttons.expand'), view: View,
tap_action: createFrigateCardSimpleAction('expand') as FrigateCardCustomAction, mediaPlayerController?: MediaPlayerManager | null,
style: options?.inExpandedMode ? this._getEmphasizedStyle() : {}, ): MenuItem | null {
}); const selectedCameraConfig = cameraManager.getStore().getCameraConfig(view.camera);
if ( if (
options?.mediaPlayerController?.hasMediaPlayers() && mediaPlayerController?.hasMediaPlayers() &&
(view?.isViewerView() || (view.is('live') && selectedCameraConfig?.camera_entity)) (view.isViewerView() || (view.is('live') && selectedCameraConfig?.camera_entity))
) { ) {
const mediaPlayerItems = options.mediaPlayerController const mediaPlayerItems = mediaPlayerController
.getMediaPlayers() .getMediaPlayers()
.map((playerEntityID) => { .map((playerEntityID) => {
const title = getEntityTitle(hass, playerEntityID) || playerEntityID; const title = getEntityTitle(hass, playerEntityID) || playerEntityID;
@@ -371,19 +484,28 @@ export class MenuButtonController {
}; };
}); });
buttons.push({ return {
icon: 'mdi:cast', icon: 'mdi:cast',
...config.menu.buttons.media_player, ...config.menu.buttons.media_player,
type: 'custom:frigate-card-menu-submenu', type: 'custom:frigate-card-menu-submenu',
title: localize('config.menu.buttons.media_player'), title: localize('config.menu.buttons.media_player'),
items: mediaPlayerItems, items: mediaPlayerItems,
}); };
}
return null;
} }
if (options?.currentMediaLoadedInfo && options.currentMediaLoadedInfo.player) { protected _getPlayPauseButton(
if (options.currentMediaLoadedInfo.capabilities?.supportsPause) { config: FrigateCardConfig,
const paused = options.currentMediaLoadedInfo.player.isPaused(); currentMediaLoadedInfo?: MediaLoadedInfo | null,
buttons.push({ ): MenuItem | null {
if (
currentMediaLoadedInfo &&
currentMediaLoadedInfo.player &&
currentMediaLoadedInfo.capabilities?.supportsPause
) {
const paused = currentMediaLoadedInfo.player.isPaused();
return {
icon: paused ? 'mdi:play' : 'mdi:pause', icon: paused ? 'mdi:play' : 'mdi:pause',
...config.menu.buttons.play, ...config.menu.buttons.play,
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
@@ -391,12 +513,22 @@ export class MenuButtonController {
tap_action: createFrigateCardSimpleAction( tap_action: createFrigateCardSimpleAction(
paused ? 'play' : 'pause', paused ? 'play' : 'pause',
) as FrigateCardCustomAction, ) as FrigateCardCustomAction,
}); };
}
return null;
} }
if (options.currentMediaLoadedInfo.capabilities?.hasAudio) { protected _getMuteUnmuteButton(
const muted = options.currentMediaLoadedInfo.player.isMuted(); config: FrigateCardConfig,
buttons.push({ currentMediaLoadedInfo?: MediaLoadedInfo | null,
): MenuItem | null {
if (
currentMediaLoadedInfo &&
currentMediaLoadedInfo.player &&
currentMediaLoadedInfo?.capabilities?.hasAudio
) {
const muted = currentMediaLoadedInfo.player.isMuted();
return {
icon: muted ? 'mdi:volume-off' : 'mdi:volume-high', icon: muted ? 'mdi:volume-off' : 'mdi:volume-high',
...config.menu.buttons.mute, ...config.menu.buttons.mute,
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
@@ -404,12 +536,17 @@ export class MenuButtonController {
tap_action: createFrigateCardSimpleAction( tap_action: createFrigateCardSimpleAction(
muted ? 'unmute' : 'mute', muted ? 'unmute' : 'mute',
) as FrigateCardCustomAction, ) as FrigateCardCustomAction,
}); };
} }
return null;
} }
if (options?.currentMediaLoadedInfo && options.currentMediaLoadedInfo.player) { protected _getScreenshotButton(
buttons.push({ config: FrigateCardConfig,
currentMediaLoadedInfo?: MediaLoadedInfo | null,
): MenuItem | null {
if (currentMediaLoadedInfo && currentMediaLoadedInfo.player) {
return {
icon: 'mdi:monitor-screenshot', icon: 'mdi:monitor-screenshot',
...config.menu.buttons.screenshot, ...config.menu.buttons.screenshot,
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
@@ -417,13 +554,20 @@ export class MenuButtonController {
tap_action: createFrigateCardSimpleAction( tap_action: createFrigateCardSimpleAction(
'screenshot', 'screenshot',
) as FrigateCardCustomAction, ) as FrigateCardCustomAction,
}); };
}
return null;
} }
protected _getDisplayModeButton(
config: FrigateCardConfig,
cameraManager: CameraManager,
view: View,
): MenuItem | null {
const viewCameraIDs = getCameraIDsForViewName(cameraManager, view.view); const viewCameraIDs = getCameraIDsForViewName(cameraManager, view.view);
if (view.supportsMultipleDisplayModes() && viewCameraIDs.size > 1) { if (view.supportsMultipleDisplayModes() && viewCameraIDs.size > 1) {
const isGrid = view.isGrid(); const isGrid = view.isGrid();
buttons.push({ return {
icon: isGrid ? 'mdi:grid-off' : 'mdi:grid', icon: isGrid ? 'mdi:grid-off' : 'mdi:grid',
...config.menu.buttons.display_mode, ...config.menu.buttons.display_mode,
style: isGrid ? this._getEmphasizedStyle() : {}, style: isGrid ? this._getEmphasizedStyle() : {},
@@ -432,30 +576,42 @@ export class MenuButtonController {
? localize('display_modes.single') ? localize('display_modes.single')
: localize('display_modes.grid'), : localize('display_modes.grid'),
tap_action: createFrigateCardDisplayModeAction(isGrid ? 'single' : 'grid'), tap_action: createFrigateCardDisplayModeAction(isGrid ? 'single' : 'grid'),
}); };
}
return null;
} }
if (hasUsablePTZ(substreamAwareCameraCapabilities, config.live.controls.ptz)) { protected _getSubstreamAwareCameraID(view: View): string {
return view.context?.live?.overrides?.get(view.camera) ?? view.camera;
}
protected _getPTZButton(
config: FrigateCardConfig,
cameraManager: CameraManager,
view: View,
): MenuItem | null {
const substreamAwareCameraCapabilities = cameraManager.getCameraCapabilities(
this._getSubstreamAwareCameraID(view),
);
if (
view.is('live') &&
hasUsablePTZ(substreamAwareCameraCapabilities, config.live.controls.ptz)
) {
const isOn = const isOn =
view.context?.live?.ptzVisible === false view.context?.live?.ptzVisible === false
? false ? false
: config.live.controls.ptz.mode === 'on'; : config.live.controls.ptz.mode === 'on';
buttons.push({ return {
icon: 'mdi:pan', icon: 'mdi:pan',
...config.menu.buttons.ptz, ...config.menu.buttons.ptz,
style: isOn ? this._getEmphasizedStyle() : {}, style: isOn ? this._getEmphasizedStyle() : {},
type: 'custom:frigate-card-menu-icon', type: 'custom:frigate-card-menu-icon',
title: localize('config.menu.buttons.ptz'), title: localize('config.menu.buttons.ptz'),
tap_action: createFrigateCardShowPTZAction(!isOn), tap_action: createFrigateCardShowPTZAction(!isOn),
}); };
} }
return null;
const styledDynamicButtons = this._dynamicMenuButtons.map((button) => ({
style: this._getStyleFromActions(config, view, button, options),
...button,
}));
return buttons.concat(styledDynamicButtons);
} }
/** /**
@@ -177,6 +177,25 @@ describe('MenuButtonController', () => {
}); });
describe('should have substream button', () => { describe('should have substream button', () => {
it('with no dependency', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ substream: true }),
},
]),
);
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
expect(buttons).not.toContainEqual(
expect.objectContaining({
title: 'Substream(s)',
}),
);
});
it('with single dependency', () => { it('with single dependency', () => {
const cameraManager = createCameraManager(); const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue( vi.mocked(cameraManager.getStore).mockReturnValue(
@@ -248,7 +267,7 @@ describe('MenuButtonController', () => {
}); });
}); });
it('with substream selected and multiple dependencies', () => { it('with substream unselected and multiple dependencies', () => {
const cameraManager = createCameraManager(); const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue( vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([ createStore([
@@ -440,7 +459,12 @@ describe('MenuButtonController', () => {
describe('should have live menu button', () => { describe('should have live menu button', () => {
it('when in live view', () => { it('when in live view', () => {
const buttons = calculateButtons(controller); const viewManager = mock<ViewManager>();
vi.mocked(viewManager.isViewSupportedByCamera).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'live' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:cctv', icon: 'mdi:cctv',
@@ -454,8 +478,12 @@ describe('MenuButtonController', () => {
}); });
it('when not in live view', () => { it('when not in live view', () => {
const view = createView({ view: 'clips' }); const viewManager = mock<ViewManager>();
const buttons = calculateButtons(controller, { view: view }); vi.mocked(viewManager.isViewSupportedByCamera).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'clips' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:cctv', icon: 'mdi:cctv',
@@ -467,6 +495,20 @@ describe('MenuButtonController', () => {
tap_action: { action: 'fire-dom-event', frigate_card_action: 'live' }, tap_action: { action: 'fire-dom-event', frigate_card_action: 'live' },
}); });
}); });
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(viewManager.isViewSupportedByCamera).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).not.toContainEqual(
expect.objectContaining({
title: 'Live view',
}),
);
});
}); });
describe('should have clips menu button', () => { describe('should have clips menu button', () => {
@@ -634,8 +676,12 @@ describe('MenuButtonController', () => {
describe('should have image menu button', () => { describe('should have image menu button', () => {
it('when in image view', () => { it('when in image view', () => {
const viewManager = mock<ViewManager>();
vi.mocked(viewManager.isViewSupportedByCamera).mockReturnValue(true);
const buttons = calculateButtons(controller, { const buttons = calculateButtons(controller, {
view: createView({ view: 'image' }), view: createView({ view: 'image' }),
viewManager: viewManager,
}); });
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
@@ -650,7 +696,13 @@ describe('MenuButtonController', () => {
}); });
it('when not in image view', () => { it('when not in image view', () => {
const buttons = calculateButtons(controller); const viewManager = mock<ViewManager>();
vi.mocked(viewManager.isViewSupportedByCamera).mockReturnValue(true);
const buttons = calculateButtons(controller, {
view: createView({ view: 'live' }),
viewManager: viewManager,
});
expect(buttons).toContainEqual({ expect(buttons).toContainEqual({
icon: 'mdi:image', icon: 'mdi:image',
@@ -662,6 +714,20 @@ describe('MenuButtonController', () => {
tap_action: { action: 'fire-dom-event', frigate_card_action: 'image' }, tap_action: { action: 'fire-dom-event', frigate_card_action: 'image' },
}); });
}); });
it('when not supported', () => {
const viewManager = mock<ViewManager>();
vi.mocked(viewManager.isViewSupportedByCamera).mockReturnValue(false);
const buttons = calculateButtons(controller, {
viewManager: viewManager,
});
expect(buttons).not.toContainEqual(
expect.objectContaining({
title: 'Static image',
}),
);
});
}); });
describe('should have timeline button', () => { describe('should have timeline button', () => {
@@ -1288,23 +1354,34 @@ describe('MenuButtonController', () => {
createCapabilities(), createCapabilities(),
); );
const buttons = calculateButtons(controller, { cameraManager: cameraManager }); const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
expect(buttons).not.toContainEqual({ view: createView({ view: 'live' }),
enabled: false,
icon: 'mdi:pan',
priority: 50,
style: {
color: 'var(--primary-color, white)',
},
tap_action: {
action: 'fire-dom-event',
frigate_card_action: 'show_ptz',
show_ptz: false,
},
title: 'Show PTZ controls',
type: 'custom:frigate-card-menu-icon',
}); });
expect(buttons).not.toContainEqual(
expect.objectContaining({
title: 'Show PTZ controls',
}),
);
});
it('when not in live view', () => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue(
createCapabilities({ ptz: { panTilt: ['relative'] } }),
);
const buttons = calculateButtons(controller, {
cameraManager: cameraManager,
view: createView({ view: 'clips' }),
});
expect(buttons).not.toContainEqual(
expect.objectContaining({
title: 'Show PTZ controls',
}),
);
}); });
it('when the selected camera is PTZ enabled', () => { it('when the selected camera is PTZ enabled', () => {