import { StyleInfo } from 'lit/directives/style-map'; import { CameraManager } from '../camera-manager/manager'; import { CallManager } from '../card-controller/call/manager'; import { FoldersManager } from '../card-controller/folders/manager'; import { FullscreenManager } from '../card-controller/fullscreen/fullscreen-manager'; import { MediaPlayerManager } from '../card-controller/media-player-manager'; import { MicrophoneManager } from '../card-controller/microphone-manager'; import { PIPManager } from '../card-controller/pip-manager'; import { ViewManager } from '../card-controller/view/view-manager'; import { AdvancedCameraCardView, VIEWS_USER_SPECIFIED, } from '../config/schema/common/const'; import { MenuItem } from '../config/schema/elements/custom/menu/types'; import { AdvancedCameraCardConfig } from '../config/schema/types'; import { getEntityTitle } from '../ha/get-entity-title'; import { HomeAssistant } from '../ha/types'; import { localize } from '../localize/localize.js'; import { MediaLoadedInfo } from '../types'; import { createCallEndAction, createCallStartAction, createCameraAction, createDisplayModeAction, createGeneralAction, createMediaPlayerAction, createPTZControlsAction, createPTZMultiAction, createSetReviewAction, createViewAction, isAdvancedCameraCardCustomAction, } from '../utils/action'; import { arrayify, isTruthy } from '../utils/basic'; import { isBeingCasted } from '../utils/casting'; import { getPTZTarget } from '../utils/ptz'; import { ViewItemClassifier } from '../view/item-classifier'; import { getStreamCameraID, hasSubstream } from '../view/substream'; import { resolveViewName } from '../view/utils/resolve-default'; import { View } from '../view/view'; import { getCameraIDsWithCapabilityForView, isViewSupported, } from '../view/view-support'; export interface MenuButtonControllerOptions { callManager?: CallManager | null; currentMediaLoadedInfo?: MediaLoadedInfo | null; showCameraUIButton?: boolean; fullscreenManager?: FullscreenManager | null; inExpandedMode?: boolean; microphoneManager?: MicrophoneManager | null; mediaPlayerController?: MediaPlayerManager | null; pipManager?: PIPManager | null; viewManager?: ViewManager | null; view?: View | null; } export class MenuButtonController { // Array of dynamic menu buttons to be added to menu. private _dynamicMenuButtons: MenuItem[] = []; public addDynamicMenuButton(button: MenuItem): void { if (!this._dynamicMenuButtons.includes(button)) { this._dynamicMenuButtons.push(button); } } public removeDynamicMenuButton(button: MenuItem): void { this._dynamicMenuButtons = this._dynamicMenuButtons.filter( (existingButton) => existingButton != button, ); } /** * Get the menu buttons to display. * @returns An array of menu buttons. */ public calculateButtons( hass: HomeAssistant, config: AdvancedCameraCardConfig, cameraManager: CameraManager, foldersManager: FoldersManager, options?: MenuButtonControllerOptions, ): MenuItem[] { const buttons: MenuItem[] = [ this._getIrisButton(config), this._getCamerasButton(config, cameraManager, options?.view), this._getSubstreamsButton(config, cameraManager, options?.view), this._getLiveButton(config, cameraManager, foldersManager, options?.view), this._getClipsButton(config, cameraManager, foldersManager, options?.view), this._getSnapshotsButton(config, cameraManager, foldersManager, options?.view), this._getRecordingsButton(config, cameraManager, foldersManager, options?.view), this._getReviewsButton(config, cameraManager, foldersManager, options?.view), this._getGalleryButton(config, cameraManager, foldersManager, options?.view), this._getImageButton(config, cameraManager, foldersManager, options?.view), this._getTimelineButton(config, cameraManager, foldersManager, options?.view), this._getDownloadButton(config, cameraManager, options?.view), this._getInfoButton(config, cameraManager, options?.view), this._getSetReviewButton(config, options?.view), this._getCameraUIButton(config, options?.showCameraUIButton), this._getCallButton(config, cameraManager, options?.callManager, options?.view), this._getMicrophoneButton( config, cameraManager, options?.view, options?.microphoneManager, options?.callManager, ), this._getExpandButton(config, options?.inExpandedMode), this._getFullscreenButton(config, options?.fullscreenManager), this._getPIPButton(config, options?.pipManager), this._getCastButton( hass, config, cameraManager, options?.view, options?.mediaPlayerController, ), this._getPlayPauseButton(config, options?.currentMediaLoadedInfo), this._getMuteUnmuteButton(config, options?.currentMediaLoadedInfo), this._getScreenshotButton(config, options?.currentMediaLoadedInfo), this._getDisplayModeButton(config, cameraManager, foldersManager, options?.view), this._getPTZControlsButton(config, cameraManager, options?.view), this._getPTZHomeButton(config, cameraManager, options?.view), this._getFoldersButton(config, foldersManager, options?.view), ...this._dynamicMenuButtons.map((button) => ({ style: this._getStyleFromActions( config, cameraManager, foldersManager, button, options, ), ...button, })), ].filter(isTruthy); return buttons; } private _getIrisButton(config: AdvancedCameraCardConfig): MenuItem { return { icon: 'iris', ...config.menu.buttons.iris, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.menu.buttons.iris'), // The default button always shows regardless of whether the menu is // hidden or not. permanent: true, tap_action: config.menu?.style === 'hidden' ? createGeneralAction('menu_toggle') : createGeneralAction('default'), hold_action: createViewAction('diagnostics'), }; } private _getCamerasButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, 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. const menuCameraIDs = cameraManager.getStore().getCameraIDsWithCapability('menu'); if (menuCameraIDs.size > 1) { const submenuItems = Array.from(menuCameraIDs, (cameraID) => { const action = createCameraAction('camera_select', cameraID); const metadata = cameraManager.getCameraMetadata(cameraID); return { enabled: true, icon: metadata?.icon.icon, entity: metadata?.icon.entity, state_color: true, title: metadata?.title, selected: view?.camera === cameraID, ...(action && { tap_action: action }), }; }); return { icon: 'mdi:video-switch', ...config.menu.buttons.cameras, type: 'custom:advanced-camera-card-menu-submenu', title: localize('config.menu.buttons.cameras'), items: submenuItems, }; } return null; } private _getSubstreamsButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, view?: View | null, ): MenuItem | null { if (!view?.camera) { return null; } const substreamCameraIDs = cameraManager .getStore() .getAllDependentCameras(view.camera, 'substream'); if (substreamCameraIDs.size && view.is('live')) { const substreams = Array.from(substreamCameraIDs).filter( (cameraID) => cameraID !== view.camera, ); const streams = [view.camera, ...substreams]; const substreamAwareCameraID = getStreamCameraID(view); if (streams.length === 2) { // If there are only two dependencies (the main camera, and 1 other) // then use a button not a menu to toggle. return { icon: 'mdi:video-input-component', style: substreamAwareCameraID !== view.camera ? this._getEmphasizedStyle() : {}, title: localize('config.menu.buttons.substreams'), ...config.menu.buttons.substreams, type: 'custom:advanced-camera-card-menu-icon', tap_action: createGeneralAction( hasSubstream(view) ? 'live_substream_off' : 'live_substream_on', ), }; } else if (streams.length > 2) { const menuItems = Array.from(streams, (streamID) => { const action = createCameraAction('live_substream_select', streamID); const metadata = cameraManager.getCameraMetadata(streamID) ?? undefined; return { enabled: true, icon: metadata?.icon.icon, entity: metadata?.icon.entity, state_color: true, title: metadata?.title, selected: substreamAwareCameraID === streamID, ...(action && { tap_action: action }), }; }); return { icon: 'mdi:video-input-component', title: localize('config.menu.buttons.substreams'), style: substreamAwareCameraID !== view.camera ? this._getEmphasizedStyle() : {}, ...config.menu.buttons.substreams, type: 'custom:advanced-camera-card-menu-submenu', items: menuItems, }; } } return null; } private _getLiveButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, foldersManager: FoldersManager, view?: View | null, ): MenuItem | null { return isViewSupported('live', cameraManager, foldersManager, view?.camera) ? { icon: 'mdi:cctv', ...config.menu.buttons.live, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.view.views.live'), style: view?.is('live') ? this._getEmphasizedStyle() : {}, tap_action: createViewAction('live'), } : null; } private _getClipsButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, foldersManager: FoldersManager, view?: View | null, ): MenuItem | null { return isViewSupported('clips', cameraManager, foldersManager, view?.camera) ? { icon: 'mdi:filmstrip', ...config.menu.buttons.clips, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.view.views.clips'), style: view?.is('clips') ? this._getEmphasizedStyle() : {}, tap_action: createViewAction('clips'), hold_action: createViewAction('clip'), } : null; } private _getSnapshotsButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, foldersManager: FoldersManager, view?: View | null, ): MenuItem | null { return isViewSupported('snapshots', cameraManager, foldersManager, view?.camera) ? { icon: 'mdi:camera', ...config.menu.buttons.snapshots, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.view.views.snapshots'), style: view?.is('snapshots') ? this._getEmphasizedStyle() : {}, tap_action: createViewAction('snapshots'), hold_action: createViewAction('snapshot'), } : null; } private _getRecordingsButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, foldersManager: FoldersManager, view?: View | null, ): MenuItem | null { return isViewSupported('recordings', cameraManager, foldersManager, view?.camera) ? { icon: 'mdi:album', ...config.menu.buttons.recordings, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.view.views.recordings'), style: view?.is('recordings') ? this._getEmphasizedStyle() : {}, tap_action: createViewAction('recordings'), hold_action: createViewAction('recording'), } : null; } private _getReviewsButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, foldersManager: FoldersManager, view?: View | null, ): MenuItem | null { return isViewSupported('reviews', cameraManager, foldersManager, view?.camera) ? { icon: 'mdi:play-box-edit-outline', ...config.menu.buttons.reviews, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.view.views.reviews'), style: view?.is('reviews') ? this._getEmphasizedStyle() : {}, tap_action: createViewAction('reviews'), hold_action: createViewAction('review'), } : null; } private _getGalleryButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, foldersManager: FoldersManager, view?: View | null, ): MenuItem | null { return isViewSupported('gallery', cameraManager, foldersManager, view?.camera) ? { icon: 'mdi:play-box-multiple', ...config.menu.buttons.gallery, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.menu.buttons.gallery'), style: view?.is('gallery') ? this._getEmphasizedStyle() : {}, tap_action: createViewAction('gallery'), hold_action: createViewAction('media'), } : null; } private _getImageButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, foldersManager: FoldersManager, view?: View | null, ): MenuItem | null { return isViewSupported('image', cameraManager, foldersManager, view?.camera) ? { icon: 'mdi:image', ...config.menu.buttons.image, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.view.views.image'), style: view?.is('image') ? this._getEmphasizedStyle() : {}, tap_action: createViewAction('image'), } : null; } private _getTimelineButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, foldersManager: FoldersManager, view?: View | null, ): MenuItem | null { return isViewSupported('timeline', cameraManager, foldersManager, view?.camera) ? { icon: 'mdi:chart-gantt', ...config.menu.buttons.timeline, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.view.views.timeline'), style: view?.is('timeline') ? this._getEmphasizedStyle() : {}, tap_action: createViewAction('timeline'), } : null; } private _getDownloadButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, view?: View | null, ): MenuItem | null { const selectedItem = view?.queryResults?.getSelectedResult(); const mediaCapabilities = selectedItem && ViewItemClassifier.isMedia(selectedItem) ? cameraManager?.getMediaCapabilities(selectedItem) : null; if (view?.isViewerView() && mediaCapabilities?.canDownload && !isBeingCasted()) { return { icon: 'mdi:download', ...config.menu.buttons.download, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.menu.buttons.download'), tap_action: createGeneralAction('download'), }; } return null; } private _getInfoButton( config: AdvancedCameraCardConfig, _cameraManager: CameraManager, view?: View | null, ): MenuItem | null { const selectedItem = view?.queryResults?.getSelectedResult(); if ( !ViewItemClassifier.isMedia(selectedItem) || !(view?.isViewerView() || view?.isGalleryView() || view?.is('timeline')) ) { return null; } return { icon: 'mdi:information-outline', ...config.menu.buttons.info, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.menu.buttons.info'), tap_action: createGeneralAction('info'), }; } private _getSetReviewButton( config: AdvancedCameraCardConfig, view?: View | null, ): MenuItem | null { const selectedItem = view?.queryResults?.getSelectedResult(); if (!view?.isViewerView() || !ViewItemClassifier.isReview(selectedItem)) { return null; } const isReviewed = selectedItem.isReviewed(); if (isReviewed === null) { return null; } return { icon: isReviewed ? 'mdi:check-circle' : 'mdi:check-circle-outline', ...config.menu.buttons.set_review, type: 'custom:advanced-camera-card-menu-icon', title: isReviewed ? localize('common.set_reviews.unreviewed') : localize('common.set_reviews.reviewed'), tap_action: createSetReviewAction(), style: isReviewed ? this._getEmphasizedStyle() : {}, }; } private _getCameraUIButton( config: AdvancedCameraCardConfig, showCameraUIButton?: boolean, ): MenuItem | null { return showCameraUIButton ? { icon: 'mdi:web', ...config.menu.buttons.camera_ui, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.menu.buttons.camera_ui'), tap_action: createGeneralAction('camera_ui'), } : null; } private _getCallButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, callManager?: CallManager | null, view?: View | null, ): MenuItem | null { if (!view?.camera || !view.is('live')) { return null; } const cameraID = view.camera; // The call targets: the selected camera and/or any 2-way-audio-capable // dependency. const targets = [ ...cameraManager.getStore().getAllDependentCameras(cameraID, '2-way-audio'), ]; if (!targets.length) { return null; } // In a call: a single hang-up button, regardless of target count. if (callManager?.isActive()) { return { icon: 'mdi:phone-hangup', title: localize('config.live.controls.call.end'), style: this._getEmphasizedStyle(true), ...config.menu.buttons.call, type: 'custom:advanced-camera-card-menu-icon', tap_action: createCallEndAction(), }; } // Idle, single target: a plain button (`call_start` resolves the default). if (targets.length === 1) { return { icon: 'mdi:phone', title: localize('config.live.controls.call.start'), ...config.menu.buttons.call, type: 'custom:advanced-camera-card-menu-icon', tap_action: createCallStartAction(), }; } // Idle, multiple targets: a submenu, one entry per stream. const menuItems = targets.map((streamID) => { const metadata = cameraManager.getCameraMetadata(streamID) ?? undefined; return { enabled: true, icon: metadata?.icon.icon, entity: metadata?.icon.entity, state_color: true, title: metadata?.title, tap_action: createCallStartAction( cameraID, streamID === cameraID ? undefined : streamID, ), }; }); return { icon: 'mdi:phone', title: localize('config.live.controls.call.start'), ...config.menu.buttons.call, type: 'custom:advanced-camera-card-menu-submenu', items: menuItems, }; } private _getMicrophoneButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, view?: View | null, microphoneManager?: MicrophoneManager | null, callManager?: CallManager | null, ): MenuItem | null { const streamCameraID = view ? getStreamCameraID(view) : null; if (!streamCameraID) { return null; } // The microphone only transmits during an active call. if (!callManager?.isActive()) { return null; } const capabilities = cameraManager.getCameraCapabilities(streamCameraID); if (microphoneManager && capabilities?.has('2-way-audio')) { const unavailable = microphoneManager.isForbidden() || !microphoneManager.isSupported(); const muted = microphoneManager.isMuted(); const buttonType = config.menu.buttons.microphone.type; return { icon: unavailable ? 'mdi:microphone-message-off' : muted ? 'mdi:microphone-off' : 'mdi:microphone', ...config.menu.buttons.microphone, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.menu.buttons.microphone'), style: unavailable || muted ? {} : this._getEmphasizedStyle(true), ...(!unavailable && buttonType === 'momentary' && { start_tap_action: createGeneralAction('microphone_unmute'), end_tap_action: createGeneralAction('microphone_mute'), }), ...(!unavailable && buttonType === 'toggle' && { tap_action: createGeneralAction( muted ? 'microphone_unmute' : 'microphone_mute', ), }), }; } return null; } private _getExpandButton( config: AdvancedCameraCardConfig, inExpandedMode?: boolean, ): MenuItem { return { icon: inExpandedMode ? 'mdi:arrow-collapse-all' : 'mdi:arrow-expand-all', ...config.menu.buttons.expand, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.menu.buttons.expand'), tap_action: createGeneralAction('expand'), style: inExpandedMode ? this._getEmphasizedStyle() : {}, }; } private _getFullscreenButton( config: AdvancedCameraCardConfig, fullscreenManager?: FullscreenManager | null, ): MenuItem | null { const inFullscreen = fullscreenManager?.isInFullscreen(); return fullscreenManager?.isSupported() ? { icon: inFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen', ...config.menu.buttons.fullscreen, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.menu.buttons.fullscreen'), tap_action: createGeneralAction('fullscreen'), style: inFullscreen ? this._getEmphasizedStyle() : {}, } : null; } private _getPIPButton( config: AdvancedCameraCardConfig, pipManager?: PIPManager | null, ): MenuItem | null { const inPIP = pipManager?.isInPIP(); return pipManager?.isAvailable() ? { icon: inPIP ? 'mdi:picture-in-picture-bottom-right-outline' : 'mdi:picture-in-picture-bottom-right', ...config.menu.buttons.pip, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.menu.buttons.pip'), tap_action: createGeneralAction('pip'), style: inPIP ? this._getEmphasizedStyle() : {}, } : null; } private _getCastButton( hass: HomeAssistant, config: AdvancedCameraCardConfig, cameraManager: CameraManager, view?: View | null, mediaPlayerController?: MediaPlayerManager | null, ): MenuItem | null { if (!view) { return null; } const selectedCameraConfig = view.camera ? cameraManager.getStore().getCameraConfig(view.camera) : null; if ( mediaPlayerController?.hasMediaPlayers() && (view.isViewerView() || (view.is('live') && selectedCameraConfig?.camera_entity)) ) { const mediaPlayerItems = mediaPlayerController .getMediaPlayers() .map((playerEntityID) => { const title = getEntityTitle(hass, playerEntityID) || playerEntityID; const state = hass.states[playerEntityID]; const playAction = createMediaPlayerAction(playerEntityID, 'play'); const stopAction = createMediaPlayerAction(playerEntityID, 'stop'); const disabled = !state || state.state === 'unavailable'; return { enabled: true, selected: false, entity: playerEntityID, state_color: false, title: title, disabled: disabled, ...(!disabled && playAction && { tap_action: playAction }), ...(!disabled && stopAction && { hold_action: stopAction }), }; }); return { icon: 'mdi:cast', ...config.menu.buttons.media_player, type: 'custom:advanced-camera-card-menu-submenu', title: localize('config.menu.buttons.media_player'), items: mediaPlayerItems, }; } return null; } private _getPlayPauseButton( config: AdvancedCameraCardConfig, currentMediaLoadedInfo?: MediaLoadedInfo | null, ): MenuItem | null { if ( currentMediaLoadedInfo && currentMediaLoadedInfo.mediaPlayerController && currentMediaLoadedInfo.capabilities?.supportsPause ) { const paused = currentMediaLoadedInfo.mediaPlayerController?.isPaused(); return { icon: paused ? 'mdi:play' : 'mdi:pause', ...config.menu.buttons.play, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.menu.buttons.play'), tap_action: createGeneralAction(paused ? 'play' : 'pause'), }; } return null; } private _getMuteUnmuteButton( config: AdvancedCameraCardConfig, currentMediaLoadedInfo?: MediaLoadedInfo | null, ): MenuItem | null { if ( currentMediaLoadedInfo && currentMediaLoadedInfo.mediaPlayerController && currentMediaLoadedInfo?.capabilities?.hasAudio ) { const muted = currentMediaLoadedInfo.mediaPlayerController?.isMuted(); return { icon: muted ? 'mdi:volume-off' : 'mdi:volume-high', ...config.menu.buttons.mute, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.menu.buttons.mute'), tap_action: createGeneralAction(muted ? 'unmute' : 'mute'), }; } return null; } private _getScreenshotButton( config: AdvancedCameraCardConfig, currentMediaLoadedInfo?: MediaLoadedInfo | null, ): MenuItem | null { if (currentMediaLoadedInfo && currentMediaLoadedInfo.mediaPlayerController) { return { icon: 'mdi:monitor-screenshot', ...config.menu.buttons.screenshot, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.menu.buttons.screenshot'), tap_action: createGeneralAction('screenshot'), }; } return null; } private _getDisplayModeButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, foldersManager: FoldersManager, view?: View | null, ): MenuItem | null { const viewCameraIDs = view ? getCameraIDsWithCapabilityForView(view.view, cameraManager, foldersManager) : null; if ( view?.supportsMultipleDisplayModes() && viewCameraIDs && viewCameraIDs.size > 1 ) { const isGrid = view.isGrid(); return { icon: isGrid ? 'mdi:grid-off' : 'mdi:grid', ...config.menu.buttons.display_mode, style: isGrid ? this._getEmphasizedStyle() : {}, type: 'custom:advanced-camera-card-menu-icon', title: isGrid ? localize('display_modes.single') : localize('display_modes.grid'), tap_action: createDisplayModeAction(isGrid ? 'single' : 'grid'), }; } return null; } private _getPTZControlsButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, view?: View | null, ): MenuItem | null { const ptzConfig = view?.is('live') ? config.live.controls.ptz : view?.isViewerView() ? config.media_viewer.controls.ptz : null; if (!view || !ptzConfig) { return null; } const ptzTarget = getPTZTarget(view, { cameraManager: cameraManager, }); if (ptzTarget) { const isOn = view.context?.ptzControls?.enabled !== undefined ? view.context.ptzControls.enabled : ptzConfig.mode === 'on' || (ptzConfig.mode === 'auto' && ptzTarget.type === 'ptz'); return { icon: 'mdi:pan', ...config.menu.buttons.ptz_controls, style: isOn ? this._getEmphasizedStyle() : {}, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.menu.buttons.ptz_controls'), tap_action: createPTZControlsAction({ enabled: !isOn }), }; } return null; } private _getPTZHomeButton( config: AdvancedCameraCardConfig, cameraManager: CameraManager, view?: View | null, ): MenuItem | null { const target = view ? getPTZTarget(view, { cameraManager: cameraManager, }) : null; if ( !target || ((target.type === 'digital' && view?.context?.zoom?.[target.targetID]?.observed?.isDefault) ?? true) ) { return null; } return { icon: 'mdi:home', ...config.menu.buttons.ptz_home, type: 'custom:advanced-camera-card-menu-icon', title: localize('config.menu.buttons.ptz_home'), tap_action: createPTZMultiAction({ targetID: target.targetID, }), }; } private _getFoldersButton( config: AdvancedCameraCardConfig, foldersManager?: FoldersManager | null, view?: View | null, ): MenuItem | null { const folders = Array.from(foldersManager?.getFolders() ?? []); if (!foldersManager?.hasFolders()) { return null; } if (folders.length === 1) { const folderID = folders[0][0]; const isSelected = !!view?.query?.getFolderQueries(folderID).length; const folder = folders[0][1]; return { icon: folder.icon ?? 'mdi:folder', ...config.menu.buttons.folders, type: 'custom:advanced-camera-card-menu-icon', title: folder.title ?? localize('config.menu.buttons.folders'), style: isSelected ? this._getEmphasizedStyle() : {}, tap_action: createViewAction('folders'), hold_action: createViewAction('folder'), }; } const submenuItems = folders.map(([id, folder]) => { const isSelected = !!view?.query?.getFolderQueries(id).length; return { enabled: true, title: folder.title ?? folder.id, icon: folder.icon ?? 'mdi:folder', selected: isSelected, style: isSelected ? this._getEmphasizedStyle() : {}, tap_action: createViewAction('folders', { folderID: id }), hold_action: createViewAction('folder', { folderID: id }), }; }); return { icon: 'mdi:folder-multiple', ...config.menu.buttons.folders, type: 'custom:advanced-camera-card-menu-submenu', title: localize('config.menu.buttons.folders'), items: submenuItems, style: view?.isAnyFolderView() ? this._getEmphasizedStyle() : {}, }; } /** * Get the style of emphasized menu items. * @returns A StyleInfo. */ private _getEmphasizedStyle(critical?: boolean): StyleInfo { if (critical) { return { animation: 'pulse 3s infinite', color: 'var(--advanced-camera-card-menu-button-critical-color)', }; } return { color: 'var(--advanced-camera-card-menu-button-active-color)', }; } /** * Given a button determine if the style should be emphasized by examining all * of the actions sequentially. * @param button The button to examine. * @returns A StyleInfo object. */ private _getStyleFromActions( config: AdvancedCameraCardConfig, cameraManager: CameraManager, foldersManager: FoldersManager, button: MenuItem, options?: MenuButtonControllerOptions, ): StyleInfo { // Review for (const actionSet of [ button.tap_action, button.double_tap_action, button.hold_action, button.start_tap_action, button.end_tap_action, ]) { for (const action of arrayify(actionSet)) { if (!isAdvancedCameraCardCustomAction(action)) { continue; } if ( VIEWS_USER_SPECIFIED.some( (viewName) => viewName === action.advanced_camera_card_action && options?.view?.is( action.advanced_camera_card_action as AdvancedCameraCardView, ), ) || (action.advanced_camera_card_action === 'default' && options?.view?.is( resolveViewName(config.view.default, cameraManager, foldersManager), )) || (action.advanced_camera_card_action === 'fullscreen' && !!options?.fullscreenManager?.isInFullscreen()) || (action.advanced_camera_card_action === 'camera_select' && options?.view?.camera === action.camera) ) { return this._getEmphasizedStyle(); } } } return {}; } }