Refactor menu buttons controller into different functions.
This commit is contained in:
@@ -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,134 +203,192 @@ 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(
|
||||||
icon: 'mdi:cctv',
|
config: FrigateCardConfig,
|
||||||
...config.menu.buttons.live,
|
view: View,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
viewManager?: ViewManager | null,
|
||||||
title: localize('config.view.views.live'),
|
): MenuItem | null {
|
||||||
style: view.is('live') ? this._getEmphasizedStyle() : {},
|
return viewManager?.isViewSupportedByCamera(view.camera, 'live')
|
||||||
tap_action: createFrigateCardSimpleAction('live') as FrigateCardCustomAction,
|
? {
|
||||||
});
|
icon: 'mdi:cctv',
|
||||||
|
...config.menu.buttons.live,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.view.views.live'),
|
||||||
|
style: view.is('live') ? this._getEmphasizedStyle() : {},
|
||||||
|
tap_action: createFrigateCardSimpleAction('live') as FrigateCardCustomAction,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'clips')) {
|
protected _getClipsButton(
|
||||||
buttons.push({
|
config: FrigateCardConfig,
|
||||||
icon: 'mdi:filmstrip',
|
view: View,
|
||||||
...config.menu.buttons.clips,
|
viewManager?: ViewManager | null,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
): MenuItem | null {
|
||||||
title: localize('config.view.views.clips'),
|
return viewManager?.isViewSupportedByCamera(view.camera, 'clips')
|
||||||
style: view?.is('clips') ? this._getEmphasizedStyle() : {},
|
? {
|
||||||
tap_action: createFrigateCardSimpleAction('clips') as FrigateCardCustomAction,
|
icon: 'mdi:filmstrip',
|
||||||
hold_action: createFrigateCardSimpleAction('clip') as FrigateCardCustomAction,
|
...config.menu.buttons.clips,
|
||||||
});
|
type: 'custom:frigate-card-menu-icon',
|
||||||
}
|
title: localize('config.view.views.clips'),
|
||||||
|
style: view?.is('clips') ? this._getEmphasizedStyle() : {},
|
||||||
|
tap_action: createFrigateCardSimpleAction('clips') as FrigateCardCustomAction,
|
||||||
|
hold_action: createFrigateCardSimpleAction('clip') as FrigateCardCustomAction,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'snapshots')) {
|
protected _getSnapshotsButton(
|
||||||
buttons.push({
|
config: FrigateCardConfig,
|
||||||
icon: 'mdi:camera',
|
view: View,
|
||||||
...config.menu.buttons.snapshots,
|
viewManager?: ViewManager | null,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
): MenuItem | null {
|
||||||
title: localize('config.view.views.snapshots'),
|
return viewManager?.isViewSupportedByCamera(view.camera, 'snapshots')
|
||||||
style: view?.is('snapshots') ? this._getEmphasizedStyle() : {},
|
? {
|
||||||
tap_action: createFrigateCardSimpleAction(
|
icon: 'mdi:camera',
|
||||||
'snapshots',
|
...config.menu.buttons.snapshots,
|
||||||
) as FrigateCardCustomAction,
|
type: 'custom:frigate-card-menu-icon',
|
||||||
hold_action: createFrigateCardSimpleAction(
|
title: localize('config.view.views.snapshots'),
|
||||||
'snapshot',
|
style: view?.is('snapshots') ? this._getEmphasizedStyle() : {},
|
||||||
) as FrigateCardCustomAction,
|
tap_action: createFrigateCardSimpleAction(
|
||||||
});
|
'snapshots',
|
||||||
}
|
) as FrigateCardCustomAction,
|
||||||
|
hold_action: createFrigateCardSimpleAction(
|
||||||
|
'snapshot',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'recordings')) {
|
protected _getRecordingsButton(
|
||||||
buttons.push({
|
config: FrigateCardConfig,
|
||||||
icon: 'mdi:album',
|
view: View,
|
||||||
...config.menu.buttons.recordings,
|
viewManager?: ViewManager | null,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
): MenuItem | null {
|
||||||
title: localize('config.view.views.recordings'),
|
return viewManager?.isViewSupportedByCamera(view.camera, 'recordings')
|
||||||
style: view.is('recordings') ? this._getEmphasizedStyle() : {},
|
? {
|
||||||
tap_action: createFrigateCardSimpleAction(
|
icon: 'mdi:album',
|
||||||
'recordings',
|
...config.menu.buttons.recordings,
|
||||||
) as FrigateCardCustomAction,
|
type: 'custom:frigate-card-menu-icon',
|
||||||
hold_action: createFrigateCardSimpleAction(
|
title: localize('config.view.views.recordings'),
|
||||||
'recording',
|
style: view.is('recordings') ? this._getEmphasizedStyle() : {},
|
||||||
) as FrigateCardCustomAction,
|
tap_action: createFrigateCardSimpleAction(
|
||||||
});
|
'recordings',
|
||||||
}
|
) as FrigateCardCustomAction,
|
||||||
|
hold_action: createFrigateCardSimpleAction(
|
||||||
|
'recording',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
buttons.push({
|
protected _getImageButton(
|
||||||
icon: 'mdi:image',
|
config: FrigateCardConfig,
|
||||||
...config.menu.buttons.image,
|
view: View,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
viewManager?: ViewManager | null,
|
||||||
title: localize('config.view.views.image'),
|
): MenuItem | null {
|
||||||
style: view?.is('image') ? this._getEmphasizedStyle() : {},
|
return viewManager?.isViewSupportedByCamera(view.camera, 'image')
|
||||||
tap_action: createFrigateCardSimpleAction('image') as FrigateCardCustomAction,
|
? {
|
||||||
});
|
icon: 'mdi:image',
|
||||||
|
...config.menu.buttons.image,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.view.views.image'),
|
||||||
|
style: view?.is('image') ? this._getEmphasizedStyle() : {},
|
||||||
|
tap_action: createFrigateCardSimpleAction('image') as FrigateCardCustomAction,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'timeline')) {
|
protected _getTimelineButton(
|
||||||
buttons.push({
|
config: FrigateCardConfig,
|
||||||
icon: 'mdi:chart-gantt',
|
view: View,
|
||||||
...config.menu.buttons.timeline,
|
viewManager?: ViewManager | null,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
): MenuItem | null {
|
||||||
title: localize('config.view.views.timeline'),
|
return viewManager?.isViewSupportedByCamera(view.camera, 'timeline')
|
||||||
style: view.is('timeline') ? this._getEmphasizedStyle() : {},
|
? {
|
||||||
tap_action: createFrigateCardSimpleAction('timeline') as FrigateCardCustomAction,
|
icon: 'mdi:chart-gantt',
|
||||||
});
|
...config.menu.buttons.timeline,
|
||||||
}
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.view.views.timeline'),
|
||||||
|
style: view.is('timeline') ? this._getEmphasizedStyle() : {},
|
||||||
|
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,
|
||||||
icon: 'mdi:web',
|
showCameraUIButton?: boolean,
|
||||||
...config.menu.buttons.camera_ui,
|
): MenuItem | null {
|
||||||
type: 'custom:frigate-card-menu-icon',
|
return showCameraUIButton
|
||||||
title: localize('config.menu.buttons.camera_ui'),
|
? {
|
||||||
tap_action: createFrigateCardSimpleAction(
|
icon: 'mdi:web',
|
||||||
'camera_ui',
|
...config.menu.buttons.camera_ui,
|
||||||
) as FrigateCardCustomAction,
|
type: 'custom:frigate-card-menu-icon',
|
||||||
});
|
title: localize('config.menu.buttons.camera_ui'),
|
||||||
}
|
tap_action: createFrigateCardSimpleAction(
|
||||||
|
'camera_ui',
|
||||||
|
) 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,
|
||||||
...config.menu.buttons.fullscreen,
|
): MenuItem {
|
||||||
type: 'custom:frigate-card-menu-icon',
|
return {
|
||||||
title: localize('config.menu.buttons.fullscreen'),
|
icon: inExpandedMode ? 'mdi:arrow-collapse-all' : 'mdi:arrow-expand-all',
|
||||||
tap_action: createFrigateCardSimpleAction(
|
|
||||||
'fullscreen',
|
|
||||||
) as FrigateCardCustomAction,
|
|
||||||
style: options?.inFullscreenMode ? this._getEmphasizedStyle() : {},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons.push({
|
|
||||||
icon: options?.inExpandedMode ? 'mdi:arrow-collapse-all' : 'mdi:arrow-expand-all',
|
|
||||||
...config.menu.buttons.expand,
|
...config.menu.buttons.expand,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.expand'),
|
title: localize('config.menu.buttons.expand'),
|
||||||
tap_action: createFrigateCardSimpleAction('expand') as FrigateCardCustomAction,
|
tap_action: createFrigateCardSimpleAction('expand') as FrigateCardCustomAction,
|
||||||
style: options?.inExpandedMode ? this._getEmphasizedStyle() : {},
|
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,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.menu.buttons.fullscreen'),
|
||||||
|
tap_action: createFrigateCardSimpleAction(
|
||||||
|
'fullscreen',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
style: inFullscreenMode ? this._getEmphasizedStyle() : {},
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _getCastButton(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: FrigateCardConfig,
|
||||||
|
cameraManager: CameraManager,
|
||||||
|
view: View,
|
||||||
|
mediaPlayerController?: MediaPlayerManager | null,
|
||||||
|
): 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,45 +484,69 @@ 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 {
|
||||||
icon: paused ? 'mdi:play' : 'mdi:pause',
|
if (
|
||||||
...config.menu.buttons.play,
|
currentMediaLoadedInfo &&
|
||||||
type: 'custom:frigate-card-menu-icon',
|
currentMediaLoadedInfo.player &&
|
||||||
title: localize('config.menu.buttons.play'),
|
currentMediaLoadedInfo.capabilities?.supportsPause
|
||||||
tap_action: createFrigateCardSimpleAction(
|
) {
|
||||||
paused ? 'play' : 'pause',
|
const paused = currentMediaLoadedInfo.player.isPaused();
|
||||||
) as FrigateCardCustomAction,
|
return {
|
||||||
});
|
icon: paused ? 'mdi:play' : 'mdi:pause',
|
||||||
}
|
...config.menu.buttons.play,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
if (options.currentMediaLoadedInfo.capabilities?.hasAudio) {
|
title: localize('config.menu.buttons.play'),
|
||||||
const muted = options.currentMediaLoadedInfo.player.isMuted();
|
tap_action: createFrigateCardSimpleAction(
|
||||||
buttons.push({
|
paused ? 'play' : 'pause',
|
||||||
icon: muted ? 'mdi:volume-off' : 'mdi:volume-high',
|
) as FrigateCardCustomAction,
|
||||||
...config.menu.buttons.mute,
|
};
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
title: localize('config.menu.buttons.mute'),
|
|
||||||
tap_action: createFrigateCardSimpleAction(
|
|
||||||
muted ? 'unmute' : 'mute',
|
|
||||||
) as FrigateCardCustomAction,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (options?.currentMediaLoadedInfo && options.currentMediaLoadedInfo.player) {
|
protected _getMuteUnmuteButton(
|
||||||
buttons.push({
|
config: FrigateCardConfig,
|
||||||
|
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',
|
||||||
|
...config.menu.buttons.mute,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.menu.buttons.mute'),
|
||||||
|
tap_action: createFrigateCardSimpleAction(
|
||||||
|
muted ? 'unmute' : 'mute',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _getScreenshotButton(
|
||||||
|
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', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user