Merge pull request #1175 from dermotduffy/menu-buttons
Refactor menu button logic out of main card
This commit is contained in:
+28
-462
@@ -10,7 +10,7 @@ import {
|
|||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
import cloneDeep from 'lodash-es/cloneDeep';
|
import cloneDeep from 'lodash-es/cloneDeep';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import merge from 'lodash-es/merge';
|
import merge from 'lodash-es/merge';
|
||||||
@@ -27,7 +27,7 @@ import { CameraManager } from './camera-manager/manager.js';
|
|||||||
import './components/elements.js';
|
import './components/elements.js';
|
||||||
import { FrigateCardElements } from './components/elements.js';
|
import { FrigateCardElements } from './components/elements.js';
|
||||||
import './components/menu.js';
|
import './components/menu.js';
|
||||||
import { FRIGATE_BUTTON_MENU_ICON, FrigateCardMenu } from './components/menu.js';
|
import { FrigateCardMenu } from './components/menu.js';
|
||||||
import './components/message.js';
|
import './components/message.js';
|
||||||
import { renderMessage, renderProgressIndicator } from './components/message.js';
|
import { renderMessage, renderProgressIndicator } from './components/message.js';
|
||||||
import './components/thumbnail-carousel.js';
|
import './components/thumbnail-carousel.js';
|
||||||
@@ -50,7 +50,6 @@ import {
|
|||||||
CardWideConfig,
|
CardWideConfig,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FRIGATE_CARD_VIEW_DEFAULT,
|
FRIGATE_CARD_VIEW_DEFAULT,
|
||||||
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
|
||||||
FrigateCardConfig,
|
FrigateCardConfig,
|
||||||
frigateCardConfigSchema,
|
frigateCardConfigSchema,
|
||||||
FrigateCardCustomAction,
|
FrigateCardCustomAction,
|
||||||
@@ -64,18 +63,14 @@ import {
|
|||||||
} from './types.js';
|
} from './types.js';
|
||||||
import {
|
import {
|
||||||
convertActionToFrigateCardCustomAction,
|
convertActionToFrigateCardCustomAction,
|
||||||
createFrigateCardCustomAction,
|
|
||||||
frigateCardHandleActionConfig,
|
frigateCardHandleActionConfig,
|
||||||
frigateCardHasAction,
|
frigateCardHasAction,
|
||||||
getActionConfigGivenAction,
|
getActionConfigGivenAction,
|
||||||
} from './utils/action.js';
|
} from './utils/action.js';
|
||||||
import { errorToConsole } from './utils/basic.js';
|
import { errorToConsole } from './utils/basic.js';
|
||||||
import { getAllDependentCameras } from './utils/camera.js';
|
|
||||||
import { log } from './utils/debug.js';
|
import { log } from './utils/debug.js';
|
||||||
import { downloadMedia } from './utils/download.js';
|
import { downloadMedia } from './utils/download.js';
|
||||||
import {
|
import {
|
||||||
getEntityIcon,
|
|
||||||
getEntityTitle,
|
|
||||||
getHassDifferences,
|
getHassDifferences,
|
||||||
isCardInPanel,
|
isCardInPanel,
|
||||||
isHassDifferent,
|
isHassDifferent,
|
||||||
@@ -90,16 +85,16 @@ import { ResolvedMediaCache } from './utils/ha/resolved-media.js';
|
|||||||
import { supportsFeature } from './utils/ha/update.js';
|
import { supportsFeature } from './utils/ha/update.js';
|
||||||
import { FrigateCardInitializer } from './utils/initializer.js';
|
import { FrigateCardInitializer } from './utils/initializer.js';
|
||||||
import { isValidMediaLoadedInfo } from './utils/media-info.js';
|
import { isValidMediaLoadedInfo } from './utils/media-info.js';
|
||||||
|
import { MenuButtonController } from './utils/menu-controller';
|
||||||
import { MicrophoneController } from './utils/microphone';
|
import { MicrophoneController } from './utils/microphone';
|
||||||
import { getActionsFromQueryString } from './utils/querystring.js';
|
import { getActionsFromQueryString } from './utils/querystring.js';
|
||||||
import {
|
import {
|
||||||
createViewWithNextStream,
|
createViewWithNextStream,
|
||||||
createViewWithoutSubstream,
|
createViewWithoutSubstream,
|
||||||
createViewWithSelectedSubstream,
|
createViewWithSelectedSubstream,
|
||||||
hasSubstream,
|
|
||||||
} from './utils/substream';
|
} from './utils/substream';
|
||||||
import { View } from './view/view.js';
|
|
||||||
import { Timer } from './utils/timer';
|
import { Timer } from './utils/timer';
|
||||||
|
import { View } from './view/view.js';
|
||||||
|
|
||||||
/** A note on media callbacks:
|
/** A note on media callbacks:
|
||||||
*
|
*
|
||||||
@@ -186,11 +181,12 @@ class FrigateCard extends LitElement {
|
|||||||
protected _panel = false;
|
protected _panel = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
protected _expand?: boolean = false;
|
protected _expand = false;
|
||||||
|
|
||||||
protected _microphoneController?: MicrophoneController;
|
protected _microphoneController?: MicrophoneController;
|
||||||
protected _conditionController?: ConditionController;
|
protected _conditionController?: ConditionController;
|
||||||
protected _automationsController?: AutomationsController;
|
protected _automationsController?: AutomationsController;
|
||||||
|
protected _menuButtonController = new MenuButtonController();
|
||||||
|
|
||||||
protected _refMenu: Ref<FrigateCardMenu> = createRef();
|
protected _refMenu: Ref<FrigateCardMenu> = createRef();
|
||||||
protected _refMain: Ref<HTMLElement> = createRef();
|
protected _refMain: Ref<HTMLElement> = createRef();
|
||||||
@@ -205,9 +201,6 @@ class FrigateCard extends LitElement {
|
|||||||
protected _currentMediaLoadedInfo: MediaLoadedInfo | null = null;
|
protected _currentMediaLoadedInfo: MediaLoadedInfo | null = null;
|
||||||
protected _lastValidMediaLoadedInfo: MediaLoadedInfo | null = null;
|
protected _lastValidMediaLoadedInfo: MediaLoadedInfo | null = null;
|
||||||
|
|
||||||
// Array of dynamic menu buttons to be added to menu.
|
|
||||||
protected _dynamicMenuButtons: MenuButton[] = [];
|
|
||||||
|
|
||||||
// Error/info message to render.
|
// Error/info message to render.
|
||||||
protected _message: Message | null = null;
|
protected _message: Message | null = null;
|
||||||
|
|
||||||
@@ -337,442 +330,6 @@ class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the style of emphasized menu items.
|
|
||||||
* @returns A StyleInfo.
|
|
||||||
*/
|
|
||||||
protected _getEmphasizedStyle(critical?: boolean): StyleInfo {
|
|
||||||
if (critical) {
|
|
||||||
return {
|
|
||||||
animation: 'pulse 3s infinite',
|
|
||||||
color: 'var(--error-color, white)',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
color: 'var(--primary-color, white)',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
protected _getStyleFromActions(button: MenuButton): StyleInfo {
|
|
||||||
for (const actionSet of [
|
|
||||||
button.tap_action,
|
|
||||||
button.double_tap_action,
|
|
||||||
button.hold_action,
|
|
||||||
button.start_tap_action,
|
|
||||||
button.end_tap_action,
|
|
||||||
]) {
|
|
||||||
const actions = Array.isArray(actionSet) ? actionSet : [actionSet];
|
|
||||||
for (const action of actions) {
|
|
||||||
// All frigate card actions will have action of 'fire-dom-event' and
|
|
||||||
// styling only applies to those.
|
|
||||||
if (
|
|
||||||
!action ||
|
|
||||||
action.action !== 'fire-dom-event' ||
|
|
||||||
!('frigate_card_action' in action)
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const frigateCardAction = action as FrigateCardCustomAction;
|
|
||||||
if (
|
|
||||||
FRIGATE_CARD_VIEWS_USER_SPECIFIED.some(
|
|
||||||
(view) =>
|
|
||||||
view === frigateCardAction.frigate_card_action &&
|
|
||||||
this._view?.is(frigateCardAction.frigate_card_action),
|
|
||||||
) ||
|
|
||||||
(frigateCardAction.frigate_card_action === 'default' &&
|
|
||||||
this._view?.is(this._getConfig().view.default)) ||
|
|
||||||
(frigateCardAction.frigate_card_action === 'fullscreen' &&
|
|
||||||
screenfull.isEnabled &&
|
|
||||||
screenfull.isFullscreen) ||
|
|
||||||
(frigateCardAction.frigate_card_action === 'camera_select' &&
|
|
||||||
this._view?.camera === frigateCardAction.camera)
|
|
||||||
) {
|
|
||||||
return this._getEmphasizedStyle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the menu buttons to display.
|
|
||||||
* @returns An array of menu buttons.
|
|
||||||
*/
|
|
||||||
protected _getMenuButtons(): MenuButton[] {
|
|
||||||
const buttons: MenuButton[] = [];
|
|
||||||
|
|
||||||
const visibleCameras = this._cameraManager?.getStore().getVisibleCameras();
|
|
||||||
const selectedCameraID = this._view?.camera;
|
|
||||||
const selectedCameraConfig = this._getSelectedCameraConfig();
|
|
||||||
const allSelectedCameraIDs = getAllDependentCameras(
|
|
||||||
this._cameraManager,
|
|
||||||
selectedCameraID,
|
|
||||||
);
|
|
||||||
const selectedMedia = this._view?.queryResults?.getSelectedResult();
|
|
||||||
|
|
||||||
const cameraCapabilities = allSelectedCameraIDs
|
|
||||||
? this._cameraManager?.getAggregateCameraCapabilities(allSelectedCameraIDs)
|
|
||||||
: null;
|
|
||||||
const mediaCapabilities = selectedMedia
|
|
||||||
? this._cameraManager?.getMediaCapabilities(selectedMedia)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
buttons.push({
|
|
||||||
// Use a magic icon value that the menu will use to render the custom
|
|
||||||
// Frigate icon.
|
|
||||||
icon: FRIGATE_BUTTON_MENU_ICON,
|
|
||||||
...this._getConfig().menu.buttons.frigate,
|
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
title: localize('config.menu.buttons.frigate'),
|
|
||||||
tap_action: FrigateCardMenu.isHidingMenu(this._getConfig().menu)
|
|
||||||
? (createFrigateCardCustomAction('menu_toggle') as FrigateCardCustomAction)
|
|
||||||
: (createFrigateCardCustomAction('default') as FrigateCardCustomAction),
|
|
||||||
hold_action: createFrigateCardCustomAction(
|
|
||||||
'diagnostics',
|
|
||||||
) as FrigateCardCustomAction,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (visibleCameras) {
|
|
||||||
const menuItems = Array.from(visibleCameras, ([cameraID, config]) => {
|
|
||||||
const action = createFrigateCardCustomAction('camera_select', {
|
|
||||||
camera: cameraID,
|
|
||||||
});
|
|
||||||
const metadata = this._hass
|
|
||||||
? this._cameraManager?.getCameraMetadata(this._hass, cameraID) ?? undefined
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled: true,
|
|
||||||
icon: metadata?.icon,
|
|
||||||
entity: config.camera_entity,
|
|
||||||
state_color: true,
|
|
||||||
title: metadata?.title,
|
|
||||||
selected: this._view?.camera === cameraID,
|
|
||||||
...(action && { tap_action: action }),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
buttons.push({
|
|
||||||
icon: 'mdi:video-switch',
|
|
||||||
...this._getConfig().menu.buttons.cameras,
|
|
||||||
type: 'custom:frigate-card-menu-submenu',
|
|
||||||
title: localize('config.menu.buttons.cameras'),
|
|
||||||
items: menuItems,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCameraID && allSelectedCameraIDs && this._view?.is('live')) {
|
|
||||||
const dependencies = [...allSelectedCameraIDs];
|
|
||||||
const override = this._view?.context?.live?.overrides?.get(selectedCameraID);
|
|
||||||
|
|
||||||
if (dependencies.length === 2) {
|
|
||||||
// If there are only two dependencies (the main camera, and 1 other)
|
|
||||||
// then use a button not a menu to toggle.
|
|
||||||
buttons.push({
|
|
||||||
icon: 'mdi:video-input-component',
|
|
||||||
style:
|
|
||||||
override && override !== selectedCameraID ? this._getEmphasizedStyle() : {},
|
|
||||||
title: localize('config.menu.buttons.substreams'),
|
|
||||||
...this._getConfig().menu.buttons.substreams,
|
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
tap_action: createFrigateCardCustomAction(
|
|
||||||
hasSubstream(this._view) ? 'live_substream_off' : 'live_substream_on',
|
|
||||||
) as FrigateCardCustomAction,
|
|
||||||
});
|
|
||||||
} else if (dependencies.length > 2) {
|
|
||||||
const menuItems = Array.from(dependencies, (cameraID) => {
|
|
||||||
const action = createFrigateCardCustomAction('live_substream_select', {
|
|
||||||
camera: cameraID,
|
|
||||||
});
|
|
||||||
const metadata = this._hass
|
|
||||||
? this._cameraManager?.getCameraMetadata(this._hass, cameraID) ?? undefined
|
|
||||||
: undefined;
|
|
||||||
const cameraConfig = this._cameraManager?.getStore().getCameraConfig(cameraID);
|
|
||||||
return {
|
|
||||||
enabled: true,
|
|
||||||
icon: metadata?.icon,
|
|
||||||
entity: cameraConfig?.camera_entity,
|
|
||||||
state_color: true,
|
|
||||||
title: metadata?.title,
|
|
||||||
selected:
|
|
||||||
(this._view?.context?.live?.overrides?.get(selectedCameraID) ??
|
|
||||||
selectedCameraID) === cameraID,
|
|
||||||
...(action && { tap_action: action }),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
buttons.push({
|
|
||||||
icon: 'mdi:video-input-component',
|
|
||||||
title: localize('config.menu.buttons.substreams'),
|
|
||||||
style:
|
|
||||||
override && override !== selectedCameraID ? this._getEmphasizedStyle() : {},
|
|
||||||
...this._getConfig().menu.buttons.substreams,
|
|
||||||
type: 'custom:frigate-card-menu-submenu',
|
|
||||||
items: menuItems,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons.push({
|
|
||||||
icon: 'mdi:cctv',
|
|
||||||
...this._getConfig().menu.buttons.live,
|
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
title: localize('config.view.views.live'),
|
|
||||||
style: this._view?.is('live') ? this._getEmphasizedStyle() : {},
|
|
||||||
tap_action: createFrigateCardCustomAction('live') as FrigateCardCustomAction,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (cameraCapabilities?.supportsClips) {
|
|
||||||
buttons.push({
|
|
||||||
icon: 'mdi:filmstrip',
|
|
||||||
...this._getConfig().menu.buttons.clips,
|
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
title: localize('config.view.views.clips'),
|
|
||||||
style: this._view?.is('clips') ? this._getEmphasizedStyle() : {},
|
|
||||||
tap_action: createFrigateCardCustomAction('clips') as FrigateCardCustomAction,
|
|
||||||
hold_action: createFrigateCardCustomAction('clip') as FrigateCardCustomAction,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cameraCapabilities?.supportsSnapshots) {
|
|
||||||
buttons.push({
|
|
||||||
icon: 'mdi:camera',
|
|
||||||
...this._getConfig().menu.buttons.snapshots,
|
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
title: localize('config.view.views.snapshots'),
|
|
||||||
style: this._view?.is('snapshots') ? this._getEmphasizedStyle() : {},
|
|
||||||
tap_action: createFrigateCardCustomAction(
|
|
||||||
'snapshots',
|
|
||||||
) as FrigateCardCustomAction,
|
|
||||||
hold_action: createFrigateCardCustomAction(
|
|
||||||
'snapshot',
|
|
||||||
) as FrigateCardCustomAction,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cameraCapabilities?.supportsRecordings) {
|
|
||||||
buttons.push({
|
|
||||||
icon: 'mdi:album',
|
|
||||||
...this._getConfig().menu.buttons.recordings,
|
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
title: localize('config.view.views.recordings'),
|
|
||||||
style: this._view?.is('recordings') ? this._getEmphasizedStyle() : {},
|
|
||||||
tap_action: createFrigateCardCustomAction(
|
|
||||||
'recordings',
|
|
||||||
) as FrigateCardCustomAction,
|
|
||||||
hold_action: createFrigateCardCustomAction(
|
|
||||||
'recording',
|
|
||||||
) as FrigateCardCustomAction,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons.push({
|
|
||||||
icon: 'mdi:image',
|
|
||||||
...this._getConfig().menu.buttons.image,
|
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
title: localize('config.view.views.image'),
|
|
||||||
style: this._view?.is('image') ? this._getEmphasizedStyle() : {},
|
|
||||||
tap_action: createFrigateCardCustomAction('image') as FrigateCardCustomAction,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Don't show the timeline button unless there's at least one non-birdseye
|
|
||||||
// camera with a Frigate camera name.
|
|
||||||
if (cameraCapabilities?.supportsTimeline) {
|
|
||||||
buttons.push({
|
|
||||||
icon: 'mdi:chart-gantt',
|
|
||||||
...this._getConfig().menu.buttons.timeline,
|
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
title: localize('config.view.views.timeline'),
|
|
||||||
style: this._view?.is('timeline') ? this._getEmphasizedStyle() : {},
|
|
||||||
tap_action: createFrigateCardCustomAction('timeline') as FrigateCardCustomAction,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaCapabilities?.canDownload && !this._isBeingCasted()) {
|
|
||||||
buttons.push({
|
|
||||||
icon: 'mdi:download',
|
|
||||||
...this._getConfig().menu.buttons.download,
|
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
title: localize('config.menu.buttons.download'),
|
|
||||||
tap_action: createFrigateCardCustomAction('download') as FrigateCardCustomAction,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._getCameraURLFromContext()) {
|
|
||||||
buttons.push({
|
|
||||||
icon: 'mdi:web',
|
|
||||||
...this._getConfig().menu.buttons.camera_ui,
|
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
title: localize('config.menu.buttons.camera_ui'),
|
|
||||||
tap_action: createFrigateCardCustomAction(
|
|
||||||
'camera_ui',
|
|
||||||
) as FrigateCardCustomAction,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this._microphoneController &&
|
|
||||||
this._currentMediaLoadedInfo?.capabilities?.supports2WayAudio
|
|
||||||
) {
|
|
||||||
const muted = this._microphoneController.isMuted();
|
|
||||||
const buttonType = this._getConfig().menu.buttons.microphone.type;
|
|
||||||
buttons.push({
|
|
||||||
icon: this._microphoneController.isForbidden()
|
|
||||||
? 'mdi:microphone-message-off'
|
|
||||||
: muted
|
|
||||||
? 'mdi:microphone-off'
|
|
||||||
: 'mdi:microphone',
|
|
||||||
...this._getConfig().menu.buttons.microphone,
|
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
title: localize('config.menu.buttons.microphone'),
|
|
||||||
style: muted ? {} : this._getEmphasizedStyle(true),
|
|
||||||
...(buttonType === 'momentary' && {
|
|
||||||
start_tap_action: createFrigateCardCustomAction(
|
|
||||||
'microphone_unmute',
|
|
||||||
) as FrigateCardCustomAction,
|
|
||||||
end_tap_action: createFrigateCardCustomAction(
|
|
||||||
'microphone_mute',
|
|
||||||
) as FrigateCardCustomAction,
|
|
||||||
}),
|
|
||||||
...(buttonType === 'toggle' && {
|
|
||||||
tap_action: createFrigateCardCustomAction(
|
|
||||||
this._microphoneController.isMuted()
|
|
||||||
? 'microphone_unmute'
|
|
||||||
: 'microphone_mute',
|
|
||||||
) as FrigateCardCustomAction,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (screenfull.isEnabled && !this._isBeingCasted()) {
|
|
||||||
buttons.push({
|
|
||||||
icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
|
|
||||||
...this._getConfig().menu.buttons.fullscreen,
|
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
title: localize('config.menu.buttons.fullscreen'),
|
|
||||||
tap_action: createFrigateCardCustomAction(
|
|
||||||
'fullscreen',
|
|
||||||
) as FrigateCardCustomAction,
|
|
||||||
style: screenfull.isFullscreen ? this._getEmphasizedStyle() : {},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons.push({
|
|
||||||
icon: this._expand ? 'mdi:arrow-collapse-all' : 'mdi:arrow-expand-all',
|
|
||||||
...this._getConfig().menu.buttons.expand,
|
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
title: localize('config.menu.buttons.expand'),
|
|
||||||
tap_action: createFrigateCardCustomAction('expand') as FrigateCardCustomAction,
|
|
||||||
style: this._expand ? this._getEmphasizedStyle() : {},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
this._mediaPlayers?.length &&
|
|
||||||
(this._view?.isViewerView() ||
|
|
||||||
(this._view?.is('live') && selectedCameraConfig?.camera_entity))
|
|
||||||
) {
|
|
||||||
const mediaPlayerItems = this._mediaPlayers.map((playerEntityID) => {
|
|
||||||
const title = getEntityTitle(this._hass, playerEntityID) || playerEntityID;
|
|
||||||
const state = this._hass?.states[playerEntityID];
|
|
||||||
const playAction = createFrigateCardCustomAction('media_player', {
|
|
||||||
media_player: playerEntityID,
|
|
||||||
media_player_action: 'play',
|
|
||||||
});
|
|
||||||
const stopAction = createFrigateCardCustomAction('media_player', {
|
|
||||||
media_player: playerEntityID,
|
|
||||||
media_player_action: 'stop',
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled: true,
|
|
||||||
selected: false,
|
|
||||||
icon: getEntityIcon(this._hass, playerEntityID) || 'mdi:cast',
|
|
||||||
entity: playerEntityID,
|
|
||||||
state_color: false,
|
|
||||||
title: title,
|
|
||||||
disabled: !state || state.state === 'unavailable',
|
|
||||||
...(playAction && { tap_action: playAction }),
|
|
||||||
...(stopAction && { hold_action: stopAction }),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
buttons.push({
|
|
||||||
icon: 'mdi:cast',
|
|
||||||
...this._getConfig().menu.buttons.media_player,
|
|
||||||
type: 'custom:frigate-card-menu-submenu',
|
|
||||||
title: localize('config.menu.buttons.media_player'),
|
|
||||||
items: mediaPlayerItems,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._currentMediaLoadedInfo && this._currentMediaLoadedInfo.player) {
|
|
||||||
if (this._currentMediaLoadedInfo.capabilities?.supportsPause) {
|
|
||||||
const paused = this._currentMediaLoadedInfo.player.isPaused();
|
|
||||||
buttons.push({
|
|
||||||
icon: paused ? 'mdi:play' : 'mdi:pause',
|
|
||||||
...this._getConfig().menu.buttons.play,
|
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
title: localize('config.menu.buttons.play'),
|
|
||||||
tap_action: createFrigateCardCustomAction(
|
|
||||||
paused ? 'play' : 'pause',
|
|
||||||
) as FrigateCardCustomAction,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._currentMediaLoadedInfo.capabilities?.hasAudio) {
|
|
||||||
const muted = this._currentMediaLoadedInfo.player.isMuted();
|
|
||||||
buttons.push({
|
|
||||||
icon: muted ? 'mdi:volume-off' : 'mdi:volume-high',
|
|
||||||
...this._getConfig().menu.buttons.mute,
|
|
||||||
type: 'custom:frigate-card-menu-icon',
|
|
||||||
title: localize('config.menu.buttons.mute'),
|
|
||||||
tap_action: createFrigateCardCustomAction(
|
|
||||||
muted ? 'unmute' : 'mute',
|
|
||||||
) as FrigateCardCustomAction,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const styledDynamicButtons = this._dynamicMenuButtons.map((button) => ({
|
|
||||||
style: this._getStyleFromActions(button),
|
|
||||||
...button,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return buttons.concat(styledDynamicButtons);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a dynamic (elements) menu button.
|
|
||||||
* @param button The button to add.
|
|
||||||
*/
|
|
||||||
public _addDynamicMenuButton(button: MenuButton): void {
|
|
||||||
if (!this._dynamicMenuButtons.includes(button)) {
|
|
||||||
this._dynamicMenuButtons = [...this._dynamicMenuButtons, button];
|
|
||||||
}
|
|
||||||
if (this._refMenu.value) {
|
|
||||||
this._refMenu.value.buttons = this._getMenuButtons();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a dynamic (elements) menu button that was previously added.
|
|
||||||
* @param target The button to remove.
|
|
||||||
*/
|
|
||||||
public _removeDynamicMenuButton(target: MenuButton): void {
|
|
||||||
this._dynamicMenuButtons = this._dynamicMenuButtons.filter(
|
|
||||||
(button) => button != target,
|
|
||||||
);
|
|
||||||
if (this._refMenu.value) {
|
|
||||||
this._refMenu.value.buttons = this._getMenuButtons();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the camera configuration for the selected camera.
|
* Get the camera configuration for the selected camera.
|
||||||
* @returns The CameraConfig object or null if not found.
|
* @returns The CameraConfig object or null if not found.
|
||||||
@@ -1860,12 +1417,27 @@ class FrigateCard extends LitElement {
|
|||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
*/
|
*/
|
||||||
protected _renderMenu(): TemplateResult | void {
|
protected _renderMenu(): TemplateResult | void {
|
||||||
|
if (!this._hass || !this._cameraManager || !this._view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
<frigate-card-menu
|
<frigate-card-menu
|
||||||
${ref(this._refMenu)}
|
${ref(this._refMenu)}
|
||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.menuConfig=${this._getConfig().menu}
|
.menuConfig=${this._getConfig().menu}
|
||||||
.buttons=${this._getMenuButtons()}
|
.buttons=${this._menuButtonController.calculateButtons(
|
||||||
|
this._hass,
|
||||||
|
this._getConfig(),
|
||||||
|
this._cameraManager,
|
||||||
|
this._view,
|
||||||
|
this._expand,
|
||||||
|
{
|
||||||
|
currentMediaLoadedInfo: this._currentMediaLoadedInfo,
|
||||||
|
mediaPlayers: this._mediaPlayers,
|
||||||
|
cameraURL: this._getCameraURLFromContext(),
|
||||||
|
microphoneController: this._microphoneController,
|
||||||
|
},
|
||||||
|
)}
|
||||||
.entityRegistryManager=${this._entityRegistryManager}
|
.entityRegistryManager=${this._entityRegistryManager}
|
||||||
></frigate-card-menu>
|
></frigate-card-menu>
|
||||||
`;
|
`;
|
||||||
@@ -1996,14 +1568,6 @@ class FrigateCard extends LitElement {
|
|||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if the card is currently being casted.
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
protected _isBeingCasted(): boolean {
|
|
||||||
return !!navigator.userAgent.match(/CrKey\//);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the aspect ratio should be enforced given the current view and
|
* Determine if the aspect ratio should be enforced given the current view and
|
||||||
* context.
|
* context.
|
||||||
@@ -2219,11 +1783,13 @@ class FrigateCard extends LitElement {
|
|||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.elements=${this._getConfig().elements}
|
.elements=${this._getConfig().elements}
|
||||||
.conditionControllerEpoch=${this._conditionController?.getEpoch()}
|
.conditionControllerEpoch=${this._conditionController?.getEpoch()}
|
||||||
@frigate-card:menu-add=${(e) => {
|
@frigate-card:menu-add=${(ev: CustomEvent<MenuButton>) => {
|
||||||
this._addDynamicMenuButton(e.detail);
|
this._menuButtonController.addDynamicMenuButton(ev.detail);
|
||||||
|
this.requestUpdate();
|
||||||
}}
|
}}
|
||||||
@frigate-card:menu-remove=${(e) => {
|
@frigate-card:menu-remove=${(ev: CustomEvent<MenuButton>) => {
|
||||||
this._removeDynamicMenuButton(e.detail);
|
this._menuButtonController.removeDynamicMenuButton(ev.detail);
|
||||||
|
this.requestUpdate();
|
||||||
}}
|
}}
|
||||||
@frigate-card:condition:evaluate=${(ev: ConditionEvaluateRequestEvent) => {
|
@frigate-card:condition:evaluate=${(ev: ConditionEvaluateRequestEvent) => {
|
||||||
ev.evaluation = this._conditionController?.evaluateCondition(ev.condition);
|
ev.evaluation = this._conditionController?.evaluateCondition(ev.condition);
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ import { FRIGATE_ICON_SVG_PATH } from '../camera-manager/frigate/icon.js';
|
|||||||
import { refreshDynamicStateParameters } from '../utils/ha';
|
import { refreshDynamicStateParameters } from '../utils/ha';
|
||||||
import './submenu.js';
|
import './submenu.js';
|
||||||
import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
|
import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
|
||||||
|
import { FRIGATE_BUTTON_MENU_ICON } from '../const.js';
|
||||||
export const FRIGATE_BUTTON_MENU_ICON = 'frigate';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A menu for the FrigateCard.
|
* A menu for the FrigateCard.
|
||||||
|
|||||||
@@ -274,3 +274,5 @@ export const MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA = 131072;
|
|||||||
// improved rendering performance.
|
// improved rendering performance.
|
||||||
export const MEDIA_CHUNK_SIZE_DEFAULT = 50;
|
export const MEDIA_CHUNK_SIZE_DEFAULT = 50;
|
||||||
export const MEDIA_CHUNK_SIZE_MAX = 1000;
|
export const MEDIA_CHUNK_SIZE_MAX = 1000;
|
||||||
|
|
||||||
|
export const FRIGATE_BUTTON_MENU_ICON = 'frigate';
|
||||||
@@ -195,6 +195,6 @@ export function domainIcon(domain: string, entity?: HassEntity, state?: string):
|
|||||||
return FIXED_DOMAIN_ICONS[domain];
|
return FIXED_DOMAIN_ICONS[domain];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn(`Unable to find icon for domain ${domain}`);
|
console.warn(`Unable to find icon for domain: ${domain}`);
|
||||||
return DEFAULT_DOMAIN_ICON;
|
return DEFAULT_DOMAIN_ICON;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,467 @@
|
|||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { StyleInfo } from 'lit/directives/style-map';
|
||||||
|
import screenfull from 'screenfull';
|
||||||
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
|
import { FRIGATE_BUTTON_MENU_ICON } from '../const';
|
||||||
|
import { localize } from '../localize/localize.js';
|
||||||
|
import {
|
||||||
|
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
||||||
|
FrigateCardConfig,
|
||||||
|
FrigateCardCustomAction,
|
||||||
|
MediaLoadedInfo,
|
||||||
|
MenuButton,
|
||||||
|
} from '../types';
|
||||||
|
import { View } from '../view/view';
|
||||||
|
import { createFrigateCardCustomAction } from './action';
|
||||||
|
import { getAllDependentCameras } from './camera';
|
||||||
|
import { getEntityIcon, getEntityTitle } from './ha';
|
||||||
|
import { MicrophoneController } from './microphone';
|
||||||
|
import { hasSubstream } from './substream';
|
||||||
|
|
||||||
|
export class MenuButtonController {
|
||||||
|
// Array of dynamic menu buttons to be added to menu.
|
||||||
|
protected _dynamicMenuButtons: MenuButton[] = [];
|
||||||
|
|
||||||
|
public addDynamicMenuButton(button: MenuButton): void {
|
||||||
|
if (!this._dynamicMenuButtons.includes(button)) {
|
||||||
|
this._dynamicMenuButtons.push(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeDynamicMenuButton(button: MenuButton): 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: FrigateCardConfig,
|
||||||
|
cameraManager: CameraManager,
|
||||||
|
view: View,
|
||||||
|
expanded: boolean,
|
||||||
|
options?: {
|
||||||
|
currentMediaLoadedInfo?: MediaLoadedInfo | null;
|
||||||
|
mediaPlayers?: string[];
|
||||||
|
cameraURL?: string | null;
|
||||||
|
microphoneController?: MicrophoneController;
|
||||||
|
},
|
||||||
|
): MenuButton[] {
|
||||||
|
const visibleCameras = cameraManager.getStore().getVisibleCameras();
|
||||||
|
const selectedCameraID = view.camera;
|
||||||
|
const selectedCameraConfig = cameraManager
|
||||||
|
.getStore()
|
||||||
|
.getCameraConfig(selectedCameraID);
|
||||||
|
const allSelectedCameraIDs = getAllDependentCameras(cameraManager, selectedCameraID);
|
||||||
|
const selectedMedia = view.queryResults?.getSelectedResult();
|
||||||
|
|
||||||
|
const cameraCapabilities =
|
||||||
|
cameraManager.getAggregateCameraCapabilities(allSelectedCameraIDs);
|
||||||
|
const mediaCapabilities = selectedMedia
|
||||||
|
? cameraManager?.getMediaCapabilities(selectedMedia)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const buttons: MenuButton[] = [];
|
||||||
|
buttons.push({
|
||||||
|
// Use a magic icon value that the menu will use to render the custom
|
||||||
|
// Frigate icon.
|
||||||
|
icon: FRIGATE_BUTTON_MENU_ICON,
|
||||||
|
...config.menu.buttons.frigate,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.menu.buttons.frigate'),
|
||||||
|
tap_action:
|
||||||
|
config.menu?.style === 'hidden'
|
||||||
|
? (createFrigateCardCustomAction('menu_toggle') as FrigateCardCustomAction)
|
||||||
|
: (createFrigateCardCustomAction('default') as FrigateCardCustomAction),
|
||||||
|
hold_action: createFrigateCardCustomAction(
|
||||||
|
'diagnostics',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (visibleCameras) {
|
||||||
|
const menuItems = Array.from(visibleCameras, ([cameraID, config]) => {
|
||||||
|
const action = createFrigateCardCustomAction('camera_select', {
|
||||||
|
camera: cameraID,
|
||||||
|
});
|
||||||
|
const metadata = cameraManager.getCameraMetadata(hass, cameraID) ?? undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
icon: metadata?.icon,
|
||||||
|
entity: config.camera_entity,
|
||||||
|
state_color: true,
|
||||||
|
title: metadata?.title,
|
||||||
|
selected: selectedCameraID === cameraID,
|
||||||
|
...(action && { tap_action: action }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
buttons.push({
|
||||||
|
icon: 'mdi:video-switch',
|
||||||
|
...config.menu.buttons.cameras,
|
||||||
|
type: 'custom:frigate-card-menu-submenu',
|
||||||
|
title: localize('config.menu.buttons.cameras'),
|
||||||
|
items: menuItems,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedCameraID && allSelectedCameraIDs && view.is('live')) {
|
||||||
|
const dependencies = [...allSelectedCameraIDs];
|
||||||
|
const override = view.context?.live?.overrides?.get(selectedCameraID);
|
||||||
|
|
||||||
|
if (dependencies.length === 2) {
|
||||||
|
// If there are only two dependencies (the main camera, and 1 other)
|
||||||
|
// then use a button not a menu to toggle.
|
||||||
|
buttons.push({
|
||||||
|
icon: 'mdi:video-input-component',
|
||||||
|
style:
|
||||||
|
override && override !== selectedCameraID ? this._getEmphasizedStyle() : {},
|
||||||
|
title: localize('config.menu.buttons.substreams'),
|
||||||
|
...config.menu.buttons.substreams,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
tap_action: createFrigateCardCustomAction(
|
||||||
|
hasSubstream(view) ? 'live_substream_off' : 'live_substream_on',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
|
} else if (dependencies.length > 2) {
|
||||||
|
const menuItems = Array.from(dependencies, (cameraID) => {
|
||||||
|
const action = createFrigateCardCustomAction('live_substream_select', {
|
||||||
|
camera: cameraID,
|
||||||
|
});
|
||||||
|
const metadata = cameraManager.getCameraMetadata(hass, cameraID) ?? undefined;
|
||||||
|
const cameraConfig = cameraManager.getStore().getCameraConfig(cameraID);
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
icon: metadata?.icon,
|
||||||
|
entity: cameraConfig?.camera_entity,
|
||||||
|
state_color: true,
|
||||||
|
title: metadata?.title,
|
||||||
|
selected:
|
||||||
|
(view.context?.live?.overrides?.get(selectedCameraID) ??
|
||||||
|
selectedCameraID) === cameraID,
|
||||||
|
...(action && { tap_action: action }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
buttons.push({
|
||||||
|
icon: 'mdi:video-input-component',
|
||||||
|
title: localize('config.menu.buttons.substreams'),
|
||||||
|
style:
|
||||||
|
override && override !== selectedCameraID ? this._getEmphasizedStyle() : {},
|
||||||
|
...config.menu.buttons.substreams,
|
||||||
|
type: 'custom:frigate-card-menu-submenu',
|
||||||
|
items: menuItems,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push({
|
||||||
|
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: createFrigateCardCustomAction('live') as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cameraCapabilities?.supportsClips) {
|
||||||
|
buttons.push({
|
||||||
|
icon: 'mdi:filmstrip',
|
||||||
|
...config.menu.buttons.clips,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.view.views.clips'),
|
||||||
|
style: view?.is('clips') ? this._getEmphasizedStyle() : {},
|
||||||
|
tap_action: createFrigateCardCustomAction('clips') as FrigateCardCustomAction,
|
||||||
|
hold_action: createFrigateCardCustomAction('clip') as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cameraCapabilities?.supportsSnapshots) {
|
||||||
|
buttons.push({
|
||||||
|
icon: 'mdi:camera',
|
||||||
|
...config.menu.buttons.snapshots,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.view.views.snapshots'),
|
||||||
|
style: view?.is('snapshots') ? this._getEmphasizedStyle() : {},
|
||||||
|
tap_action: createFrigateCardCustomAction(
|
||||||
|
'snapshots',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
hold_action: createFrigateCardCustomAction(
|
||||||
|
'snapshot',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cameraCapabilities?.supportsRecordings) {
|
||||||
|
buttons.push({
|
||||||
|
icon: 'mdi:album',
|
||||||
|
...config.menu.buttons.recordings,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.view.views.recordings'),
|
||||||
|
style: view.is('recordings') ? this._getEmphasizedStyle() : {},
|
||||||
|
tap_action: createFrigateCardCustomAction(
|
||||||
|
'recordings',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
hold_action: createFrigateCardCustomAction(
|
||||||
|
'recording',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push({
|
||||||
|
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: createFrigateCardCustomAction('image') as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't show the timeline button unless there's at least one non-birdseye
|
||||||
|
// camera with a Frigate camera name.
|
||||||
|
if (cameraCapabilities?.supportsTimeline) {
|
||||||
|
buttons.push({
|
||||||
|
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: createFrigateCardCustomAction('timeline') as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaCapabilities?.canDownload && !this._isBeingCasted()) {
|
||||||
|
buttons.push({
|
||||||
|
icon: 'mdi:download',
|
||||||
|
...config.menu.buttons.download,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.menu.buttons.download'),
|
||||||
|
tap_action: createFrigateCardCustomAction('download') as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.cameraURL) {
|
||||||
|
buttons.push({
|
||||||
|
icon: 'mdi:web',
|
||||||
|
...config.menu.buttons.camera_ui,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.menu.buttons.camera_ui'),
|
||||||
|
tap_action: createFrigateCardCustomAction(
|
||||||
|
'camera_ui',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
options?.microphoneController &&
|
||||||
|
options?.currentMediaLoadedInfo?.capabilities?.supports2WayAudio
|
||||||
|
) {
|
||||||
|
const forbidden = options.microphoneController.isForbidden();
|
||||||
|
const muted = options.microphoneController.isMuted();
|
||||||
|
const buttonType = config.menu.buttons.microphone.type;
|
||||||
|
buttons.push({
|
||||||
|
icon: forbidden
|
||||||
|
? 'mdi:microphone-message-off'
|
||||||
|
: muted
|
||||||
|
? 'mdi:microphone-off'
|
||||||
|
: 'mdi:microphone',
|
||||||
|
...config.menu.buttons.microphone,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.menu.buttons.microphone'),
|
||||||
|
style: forbidden || muted ? {} : this._getEmphasizedStyle(true),
|
||||||
|
...(!forbidden &&
|
||||||
|
buttonType === 'momentary' && {
|
||||||
|
start_tap_action: createFrigateCardCustomAction(
|
||||||
|
'microphone_unmute',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
end_tap_action: createFrigateCardCustomAction(
|
||||||
|
'microphone_mute',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
}),
|
||||||
|
...(!forbidden &&
|
||||||
|
buttonType === 'toggle' && {
|
||||||
|
tap_action: createFrigateCardCustomAction(
|
||||||
|
options.microphoneController.isMuted()
|
||||||
|
? 'microphone_unmute'
|
||||||
|
: 'microphone_mute',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screenfull.isEnabled && !this._isBeingCasted()) {
|
||||||
|
buttons.push({
|
||||||
|
icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
|
||||||
|
...config.menu.buttons.fullscreen,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.menu.buttons.fullscreen'),
|
||||||
|
tap_action: createFrigateCardCustomAction(
|
||||||
|
'fullscreen',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
style: screenfull.isFullscreen ? this._getEmphasizedStyle() : {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push({
|
||||||
|
icon: expanded ? '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: createFrigateCardCustomAction('expand') as FrigateCardCustomAction,
|
||||||
|
style: expanded ? this._getEmphasizedStyle() : {},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
options?.mediaPlayers?.length &&
|
||||||
|
(view?.isViewerView() || (view.is('live') && selectedCameraConfig?.camera_entity))
|
||||||
|
) {
|
||||||
|
const mediaPlayerItems = options.mediaPlayers.map((playerEntityID) => {
|
||||||
|
const title = getEntityTitle(hass, playerEntityID) || playerEntityID;
|
||||||
|
const state = hass.states[playerEntityID];
|
||||||
|
const playAction = createFrigateCardCustomAction('media_player', {
|
||||||
|
media_player: playerEntityID,
|
||||||
|
media_player_action: 'play',
|
||||||
|
});
|
||||||
|
const stopAction = createFrigateCardCustomAction('media_player', {
|
||||||
|
media_player: playerEntityID,
|
||||||
|
media_player_action: 'stop',
|
||||||
|
});
|
||||||
|
const disabled = !state || state.state === 'unavailable';
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
selected: false,
|
||||||
|
icon: getEntityIcon(hass, playerEntityID),
|
||||||
|
entity: playerEntityID,
|
||||||
|
state_color: false,
|
||||||
|
title: title,
|
||||||
|
disabled: disabled,
|
||||||
|
...(!disabled && playAction && { tap_action: playAction }),
|
||||||
|
...(!disabled && stopAction && { hold_action: stopAction }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
buttons.push({
|
||||||
|
icon: 'mdi:cast',
|
||||||
|
...config.menu.buttons.media_player,
|
||||||
|
type: 'custom:frigate-card-menu-submenu',
|
||||||
|
title: localize('config.menu.buttons.media_player'),
|
||||||
|
items: mediaPlayerItems,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.currentMediaLoadedInfo && options.currentMediaLoadedInfo.player) {
|
||||||
|
if (options.currentMediaLoadedInfo.capabilities?.supportsPause) {
|
||||||
|
const paused = options.currentMediaLoadedInfo.player.isPaused();
|
||||||
|
buttons.push({
|
||||||
|
icon: paused ? 'mdi:play' : 'mdi:pause',
|
||||||
|
...config.menu.buttons.play,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.menu.buttons.play'),
|
||||||
|
tap_action: createFrigateCardCustomAction(
|
||||||
|
paused ? 'play' : 'pause',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.currentMediaLoadedInfo.capabilities?.hasAudio) {
|
||||||
|
const muted = options.currentMediaLoadedInfo.player.isMuted();
|
||||||
|
buttons.push({
|
||||||
|
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: createFrigateCardCustomAction(
|
||||||
|
muted ? 'unmute' : 'mute',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const styledDynamicButtons = this._dynamicMenuButtons.map((button) => ({
|
||||||
|
style: this._getStyleFromActions(config, view, button),
|
||||||
|
...button,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return buttons.concat(styledDynamicButtons);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the style of emphasized menu items.
|
||||||
|
* @returns A StyleInfo.
|
||||||
|
*/
|
||||||
|
protected _getEmphasizedStyle(critical?: boolean): StyleInfo {
|
||||||
|
if (critical) {
|
||||||
|
return {
|
||||||
|
animation: 'pulse 3s infinite',
|
||||||
|
color: 'var(--error-color, white)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
color: 'var(--primary-color, white)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
protected _getStyleFromActions(
|
||||||
|
config: FrigateCardConfig,
|
||||||
|
view: View,
|
||||||
|
button: MenuButton,
|
||||||
|
): StyleInfo {
|
||||||
|
for (const actionSet of [
|
||||||
|
button.tap_action,
|
||||||
|
button.double_tap_action,
|
||||||
|
button.hold_action,
|
||||||
|
button.start_tap_action,
|
||||||
|
button.end_tap_action,
|
||||||
|
]) {
|
||||||
|
const actions = Array.isArray(actionSet) ? actionSet : [actionSet];
|
||||||
|
for (const action of actions) {
|
||||||
|
// All frigate card actions will have action of 'fire-dom-event' and
|
||||||
|
// styling only applies to those.
|
||||||
|
if (
|
||||||
|
!action ||
|
||||||
|
action.action !== 'fire-dom-event' ||
|
||||||
|
!('frigate_card_action' in action)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const frigateCardAction = action as FrigateCardCustomAction;
|
||||||
|
if (
|
||||||
|
FRIGATE_CARD_VIEWS_USER_SPECIFIED.some(
|
||||||
|
(viewName) =>
|
||||||
|
viewName === frigateCardAction.frigate_card_action &&
|
||||||
|
view?.is(frigateCardAction.frigate_card_action),
|
||||||
|
) ||
|
||||||
|
(frigateCardAction.frigate_card_action === 'default' &&
|
||||||
|
view.is(config.view.default)) ||
|
||||||
|
(frigateCardAction.frigate_card_action === 'fullscreen' &&
|
||||||
|
screenfull.isEnabled &&
|
||||||
|
screenfull.isFullscreen) ||
|
||||||
|
(frigateCardAction.frigate_card_action === 'camera_select' &&
|
||||||
|
view.camera === frigateCardAction.camera)
|
||||||
|
) {
|
||||||
|
return this._getEmphasizedStyle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the card is currently being casted.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
protected _isBeingCasted(): boolean {
|
||||||
|
return !!navigator.userAgent.match(/CrKey\//);
|
||||||
|
}
|
||||||
|
}
|
||||||
+82
-4
@@ -1,19 +1,31 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
|
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
|
||||||
|
import { vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { CameraManagerEngineFactory } from '../src/camera-manager/engine-factory';
|
||||||
import { FrigateEvent, FrigateRecording } from '../src/camera-manager/frigate/types';
|
import { FrigateEvent, FrigateRecording } from '../src/camera-manager/frigate/types';
|
||||||
|
import { CameraManager } from '../src/camera-manager/manager';
|
||||||
|
import { CameraManagerStore } from '../src/camera-manager/store';
|
||||||
|
import {
|
||||||
|
CameraConfigs,
|
||||||
|
CameraManagerCameraCapabilities,
|
||||||
|
CameraManagerMediaCapabilities,
|
||||||
|
} from '../src/camera-manager/types';
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
FrigateCardCondition,
|
FrigateCardCondition,
|
||||||
FrigateCardConfig,
|
FrigateCardConfig,
|
||||||
|
MediaLoadedInfo,
|
||||||
|
RawFrigateCardConfig,
|
||||||
cameraConfigSchema,
|
cameraConfigSchema,
|
||||||
frigateCardConditionSchema,
|
frigateCardConditionSchema,
|
||||||
frigateCardConfigSchema,
|
frigateCardConfigSchema,
|
||||||
} from '../src/types';
|
} from '../src/types';
|
||||||
import { Entity } from '../src/utils/ha/entity-registry/types';
|
import { Entity } from '../src/utils/ha/entity-registry/types';
|
||||||
|
import { View, ViewParameters } from '../src/view/view';
|
||||||
|
|
||||||
export const createCameraConfig = (config: unknown): CameraConfig => {
|
export const createCameraConfig = (config?: unknown): CameraConfig => {
|
||||||
return cameraConfigSchema.parse(config);
|
return cameraConfigSchema.parse(config ?? {});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createCondition = (
|
export const createCondition = (
|
||||||
@@ -22,8 +34,12 @@ export const createCondition = (
|
|||||||
return frigateCardConditionSchema.parse(condition ?? {});
|
return frigateCardConditionSchema.parse(condition ?? {});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createConfig = (config?: Partial<FrigateCardConfig>): FrigateCardConfig => {
|
export const createConfig = (config?: RawFrigateCardConfig): FrigateCardConfig => {
|
||||||
return frigateCardConfigSchema.parse(config);
|
return frigateCardConfigSchema.parse({
|
||||||
|
type: 'frigate-hass-card',
|
||||||
|
cameras: [],
|
||||||
|
...config,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createHASS = (states?: HassEntities): HomeAssistant => {
|
export const createHASS = (states?: HassEntities): HomeAssistant => {
|
||||||
@@ -89,3 +105,65 @@ export const createFrigateRecording = (recording?: Partial<FrigateRecording>) =>
|
|||||||
...recording,
|
...recording,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createView = (options?: Partial<ViewParameters>): View => {
|
||||||
|
return new View({
|
||||||
|
view: 'live',
|
||||||
|
camera: 'camera',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createCameraManager = (options?: {
|
||||||
|
store?: CameraManagerStore;
|
||||||
|
configs?: CameraConfigs;
|
||||||
|
}): CameraManager => {
|
||||||
|
const cameraManager = new CameraManager(mock<CameraManagerEngineFactory>(), {});
|
||||||
|
let store: CameraManagerStore | undefined = options?.store;
|
||||||
|
if (!store) {
|
||||||
|
store = mock<CameraManagerStore>();
|
||||||
|
const configs = options?.configs ?? new Map([['camera', createCameraConfig()]]);
|
||||||
|
vi.mocked(store.getCameras).mockReturnValue(configs);
|
||||||
|
vi.mocked(store.getVisibleCameras).mockReturnValue(configs);
|
||||||
|
vi.mocked(store.getCameraConfig).mockImplementation((cameraID): CameraConfig => {
|
||||||
|
return configs.get(cameraID) ?? createCameraConfig();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(store);
|
||||||
|
return cameraManager;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createCameraCapabilities = (
|
||||||
|
options?: Partial<CameraManagerCameraCapabilities>,
|
||||||
|
): CameraManagerCameraCapabilities => {
|
||||||
|
return {
|
||||||
|
canFavoriteEvents: false,
|
||||||
|
canFavoriteRecordings: false,
|
||||||
|
canSeek: false,
|
||||||
|
supportsClips: false,
|
||||||
|
supportsRecordings: false,
|
||||||
|
supportsSnapshots: false,
|
||||||
|
supportsTimeline: false,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createMediaCapabilities = (
|
||||||
|
options?: Partial<CameraManagerMediaCapabilities>,
|
||||||
|
): CameraManagerMediaCapabilities => {
|
||||||
|
return {
|
||||||
|
canFavorite: false,
|
||||||
|
canDownload: false,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createMediaLoadedInfo = (
|
||||||
|
options?: Partial<MediaLoadedInfo>,
|
||||||
|
): MediaLoadedInfo => {
|
||||||
|
return {
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { CameraManagerEngineFactory } from '../../src/camera-manager/engine-factory.js';
|
|
||||||
import { CameraManager } from '../../src/camera-manager/manager.js';
|
import { CameraManager } from '../../src/camera-manager/manager.js';
|
||||||
import { CameraManagerStore } from '../../src/camera-manager/store.js';
|
|
||||||
import { CameraConfigs } from '../../src/camera-manager/types.js';
|
import { CameraConfigs } from '../../src/camera-manager/types.js';
|
||||||
import { getAllDependentCameras, getCameraID } from '../../src/utils/camera.js';
|
import { getAllDependentCameras, getCameraID } from '../../src/utils/camera.js';
|
||||||
import { createCameraConfig } from '../test-utils.js';
|
import { createCameraConfig, createCameraManager } from '../test-utils.js';
|
||||||
|
|
||||||
vi.mock('../../src/camera-manager/manager.js');
|
vi.mock('../../src/camera-manager/manager.js');
|
||||||
|
|
||||||
@@ -54,11 +52,7 @@ describe('getAllDependentCameras', () => {
|
|||||||
['two', createCameraConfig({})],
|
['two', createCameraConfig({})],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const cameraManager = new CameraManager(mock<CameraManagerEngineFactory>(), {});
|
const cameraManager = createCameraManager({ configs: cameraConfigs });
|
||||||
const store = mock<CameraManagerStore>();
|
|
||||||
vi.mocked(cameraManager.getStore).mockReturnValue(store);
|
|
||||||
store.getCameras.mockReturnValue(cameraConfigs);
|
|
||||||
|
|
||||||
expect(getAllDependentCameras(cameraManager, 'one')).toEqual(
|
expect(getAllDependentCameras(cameraManager, 'one')).toEqual(
|
||||||
new Set(['one', 'two']),
|
new Set(['one', 'two']),
|
||||||
);
|
);
|
||||||
@@ -76,11 +70,7 @@ describe('getAllDependentCameras', () => {
|
|||||||
['two', createCameraConfig({})],
|
['two', createCameraConfig({})],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const cameraManager = new CameraManager(mock<CameraManagerEngineFactory>(), {});
|
const cameraManager = createCameraManager({ configs: cameraConfigs });
|
||||||
const store = mock<CameraManagerStore>();
|
|
||||||
vi.mocked(cameraManager.getStore).mockReturnValue(store);
|
|
||||||
store.getCameras.mockReturnValue(cameraConfigs);
|
|
||||||
|
|
||||||
expect(getAllDependentCameras(cameraManager, 'one')).toEqual(
|
expect(getAllDependentCameras(cameraManager, 'one')).toEqual(
|
||||||
new Set(['one', 'two']),
|
new Set(['one', 'two']),
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,4 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
import { CameraManager } from '../../src/camera-manager/manager';
|
|
||||||
import { getAllDependentCameras } from '../../src/utils/camera';
|
import { getAllDependentCameras } from '../../src/utils/camera';
|
||||||
import {
|
import {
|
||||||
createViewWithNextStream,
|
createViewWithNextStream,
|
||||||
@@ -9,7 +7,9 @@ import {
|
|||||||
hasSubstream,
|
hasSubstream,
|
||||||
} from '../../src/utils/substream';
|
} from '../../src/utils/substream';
|
||||||
import { View } from '../../src/view/view';
|
import { View } from '../../src/view/view';
|
||||||
|
import { createCameraManager } from '../test-utils';
|
||||||
|
|
||||||
|
vi.mock('../../src/camera-manager/manager.js');
|
||||||
vi.mock('../../src/utils/camera');
|
vi.mock('../../src/utils/camera');
|
||||||
|
|
||||||
describe('createViewWithSelectedSubstream', () => {
|
describe('createViewWithSelectedSubstream', () => {
|
||||||
@@ -95,7 +95,7 @@ describe('createViewWithNextStream', () => {
|
|||||||
camera: 'camera',
|
camera: 'camera',
|
||||||
});
|
});
|
||||||
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera']));
|
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera']));
|
||||||
const cameraManager = mock<CameraManager>();
|
const cameraManager = createCameraManager()
|
||||||
const newView = createViewWithNextStream(cameraManager, view);
|
const newView = createViewWithNextStream(cameraManager, view);
|
||||||
expect(newView.camera).toBe(view.camera);
|
expect(newView.camera).toBe(view.camera);
|
||||||
expect(newView.view).toBe(view.view);
|
expect(newView.view).toBe(view.view);
|
||||||
@@ -107,7 +107,7 @@ describe('createViewWithNextStream', () => {
|
|||||||
camera: 'camera',
|
camera: 'camera',
|
||||||
});
|
});
|
||||||
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera', 'camera2']));
|
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera', 'camera2']));
|
||||||
const cameraManager = mock<CameraManager>();
|
const cameraManager = createCameraManager()
|
||||||
const newView = createViewWithNextStream(cameraManager, view);
|
const newView = createViewWithNextStream(cameraManager, view);
|
||||||
expect(newView.context?.live?.overrides).toEqual(new Map([['camera', 'camera2']]));
|
expect(newView.context?.live?.overrides).toEqual(new Map([['camera', 'camera2']]));
|
||||||
});
|
});
|
||||||
@@ -122,7 +122,7 @@ describe('createViewWithNextStream', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera', 'camera2']));
|
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera', 'camera2']));
|
||||||
const cameraManager = mock<CameraManager>();
|
const cameraManager = createCameraManager()
|
||||||
const newView = createViewWithNextStream(cameraManager, view);
|
const newView = createViewWithNextStream(cameraManager, view);
|
||||||
expect(newView.context?.live?.overrides).toEqual(new Map([['camera', 'camera']]));
|
expect(newView.context?.live?.overrides).toEqual(new Map([['camera', 'camera']]));
|
||||||
});
|
});
|
||||||
@@ -137,7 +137,7 @@ describe('createViewWithNextStream', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera', 'camera2']));
|
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera', 'camera2']));
|
||||||
const cameraManager = mock<CameraManager>();
|
const cameraManager = createCameraManager()
|
||||||
const newView = createViewWithNextStream(cameraManager, view);
|
const newView = createViewWithNextStream(cameraManager, view);
|
||||||
expect(newView.context?.live?.overrides).toEqual(new Map([['camera', 'camera']]));
|
expect(newView.context?.live?.overrides).toEqual(new Map([['camera', 'camera']]));
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-15
@@ -1,22 +1,10 @@
|
|||||||
import { describe, it, expect, vi, test } from 'vitest';
|
import { describe, expect, it, test, vi } from 'vitest';
|
||||||
import { QueryType } from '../../src/camera-manager/types';
|
import { QueryType } from '../../src/camera-manager/types';
|
||||||
import { ViewMedia } from '../../src/view/media';
|
import { ViewMedia } from '../../src/view/media';
|
||||||
import { EventMediaQueries, RecordingMediaQueries } from '../../src/view/media-queries';
|
import { EventMediaQueries, RecordingMediaQueries } from '../../src/view/media-queries';
|
||||||
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
||||||
import {
|
import { View, dispatchViewContextChangeEvent } from '../../src/view/view';
|
||||||
View,
|
import { createView } from '../test-utils';
|
||||||
ViewParameters,
|
|
||||||
dispatchViewContextChangeEvent,
|
|
||||||
} from '../../src/view/view';
|
|
||||||
import { ViewContext } from 'view';
|
|
||||||
|
|
||||||
const createView = (options?: Partial<ViewParameters>): View => {
|
|
||||||
return new View({
|
|
||||||
...options,
|
|
||||||
view: options?.view ?? 'live',
|
|
||||||
camera: options?.camera ?? 'camera',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
describe('View Basics', () => {
|
describe('View Basics', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user