Merge pull request #1396 from dermotduffy/better-button-accuracy

Ensure timeline menu button shows up when it should
This commit is contained in:
Dermot Duffy
2024-03-09 20:01:42 -08:00
committed by GitHub
7 changed files with 1208 additions and 1070 deletions
+2 -7
View File
@@ -3,7 +3,6 @@ import {
ActionsConfig,
ActionType,
FrigateCardCustomAction,
FRIGATE_CARD_VIEW_DEFAULT,
} from '../config/types.js';
import {
convertActionToFrigateCardCustomAction,
@@ -177,14 +176,10 @@ export class ActionsManager {
const viewOnCameraSelect = config?.view.camera_select ?? 'current';
const targetViewName =
viewOnCameraSelect === 'current' ? view.view : viewOnCameraSelect;
const verifiedViewName = this._api
.getViewManager()
.isViewSupportedByCamera(selectCameraID, targetViewName)
? targetViewName
: FRIGATE_CARD_VIEW_DEFAULT;
this._api.getViewManager().setViewByParameters({
viewName: verifiedViewName,
viewName: targetViewName,
cameraID: selectCameraID,
failSafe: true,
});
}
break;
+20 -8
View File
@@ -190,7 +190,17 @@ export class ViewManager {
}
public isViewSupportedByCamera(cameraID: string, view: FrigateCardView): boolean {
const capabilities = this._api.getCameraManager().getCameraCapabilities(cameraID);
const dependentCamerasCapabilities = this._api
.getCameraManager()
.getAggregateCameraCapabilities(
this._api.getCameraManager().getStore().getAllDependentCameras(cameraID),
);
const allCamerasCapabilities = this._api
.getCameraManager()
.getAggregateCameraCapabilities(
this._api.getCameraManager().getStore().getCameraIDs(),
);
switch (view) {
case 'live':
case 'image':
@@ -198,20 +208,22 @@ export class ViewManager {
return true;
case 'clip':
case 'clips':
return !!capabilities?.supportsClips;
return !!dependentCamerasCapabilities?.supportsClips;
case 'snapshot':
case 'snapshots':
return !!capabilities?.supportsSnapshots;
return !!dependentCamerasCapabilities?.supportsSnapshots;
case 'recording':
case 'recordings':
return !!capabilities?.supportsRecordings;
return !!dependentCamerasCapabilities?.supportsRecordings;
case 'timeline':
return !!capabilities?.supportsTimeline;
// Show the timeline if any camera supports it, even cameras unrelated
// to the currently selected camera.
return !!allCamerasCapabilities?.supportsTimeline;
case 'media':
return (
!!capabilities?.supportsClips ||
!!capabilities?.supportsSnapshots ||
!!capabilities?.supportsRecordings
!!dependentCamerasCapabilities?.supportsClips ||
!!dependentCamerasCapabilities?.supportsSnapshots ||
!!dependentCamerasCapabilities?.supportsRecordings
);
}
}
+1
View File
@@ -209,6 +209,7 @@ class FrigateCard extends LitElement {
showCameraUIButton: this._controller.getCameraURLManager().hasCameraURL(),
mediaPlayerController: this._controller.getMediaPlayerManager(),
microphoneManager: this._controller.getMicrophoneManager(),
viewManager: this._controller.getViewManager(),
},
)}
.entityRegistryManager=${this._controller.getEntityRegistryManager()}
+20 -16
View File
@@ -3,10 +3,11 @@ import { StyleInfo } from 'lit/directives/style-map';
import { CameraManager } from '../camera-manager/manager';
import { MediaPlayerManager } from '../card-controller/media-player-manager';
import { MicrophoneManager } from '../card-controller/microphone-manager';
import { ViewManager } from '../card-controller/view-manager';
import {
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
FrigateCardConfig,
FrigateCardCustomAction,
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
MenuItem,
} from '../config/types';
import { FRIGATE_BUTTON_MENU_ICON } from '../const';
@@ -31,6 +32,7 @@ export interface MenuButtonControllerOptions {
inExpandedMode?: boolean;
microphoneManager?: MicrophoneManager | null;
mediaPlayerController?: MediaPlayerManager | null;
viewManager?: ViewManager | null;
}
export class MenuButtonController {
@@ -62,18 +64,19 @@ export class MenuButtonController {
): MenuItem[] {
const visibleCameraIDs = cameraManager.getStore().getVisibleCameraIDs();
const selectedCameraID = view.camera;
const substreamAwareCameraID =
view.context?.live?.overrides?.get(selectedCameraID) ?? selectedCameraID;
const selectedCameraConfig = cameraManager
.getStore()
.getCameraConfig(selectedCameraID);
const allSelectedCameraIDs = cameraManager
.getStore()
.getAllDependentCameras(selectedCameraID);
const selectedMedia = view.queryResults?.getSelectedResult();
const selectedCameraCapabilities =
cameraManager.getCameraCapabilities(selectedCameraID);
const aggregateCapabilities =
cameraManager.getAggregateCameraCapabilities(allSelectedCameraIDs);
const substreamAwareCameraCapabilities =
cameraManager.getCameraCapabilities(substreamAwareCameraID);
const selectedMedia = view.queryResults?.getSelectedResult();
const mediaCapabilities = selectedMedia
? cameraManager?.getMediaCapabilities(selectedMedia)
: null;
@@ -125,7 +128,6 @@ export class MenuButtonController {
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)
@@ -133,7 +135,9 @@ export class MenuButtonController {
buttons.push({
icon: 'mdi:video-input-component',
style:
override && override !== selectedCameraID ? this._getEmphasizedStyle() : {},
substreamAwareCameraID !== selectedCameraID
? this._getEmphasizedStyle()
: {},
title: localize('config.menu.buttons.substreams'),
...config.menu.buttons.substreams,
type: 'custom:frigate-card-menu-icon',
@@ -166,7 +170,9 @@ export class MenuButtonController {
icon: 'mdi:video-input-component',
title: localize('config.menu.buttons.substreams'),
style:
override && override !== selectedCameraID ? this._getEmphasizedStyle() : {},
substreamAwareCameraID !== selectedCameraID
? this._getEmphasizedStyle()
: {},
...config.menu.buttons.substreams,
type: 'custom:frigate-card-menu-submenu',
items: menuItems,
@@ -183,7 +189,7 @@ export class MenuButtonController {
tap_action: createFrigateCardSimpleAction('live') as FrigateCardCustomAction,
});
if (aggregateCapabilities?.supportsClips) {
if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'clips')) {
buttons.push({
icon: 'mdi:filmstrip',
...config.menu.buttons.clips,
@@ -195,7 +201,7 @@ export class MenuButtonController {
});
}
if (aggregateCapabilities?.supportsSnapshots) {
if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'snapshots')) {
buttons.push({
icon: 'mdi:camera',
...config.menu.buttons.snapshots,
@@ -211,7 +217,7 @@ export class MenuButtonController {
});
}
if (aggregateCapabilities?.supportsRecordings) {
if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'recordings')) {
buttons.push({
icon: 'mdi:album',
...config.menu.buttons.recordings,
@@ -236,9 +242,7 @@ export class MenuButtonController {
tap_action: createFrigateCardSimpleAction('image') as FrigateCardCustomAction,
});
// Don't show the timeline button unless there's at least one non-birdseye
// camera with a Frigate camera name.
if (aggregateCapabilities?.supportsTimeline) {
if (options?.viewManager?.isViewSupportedByCamera(selectedCameraID, 'timeline')) {
buttons.push({
icon: 'mdi:chart-gantt',
...config.menu.buttons.timeline,
@@ -419,7 +423,7 @@ export class MenuButtonController {
});
}
if (hasUsablePTZ(selectedCameraCapabilities, config.live.controls.ptz)) {
if (hasUsablePTZ(substreamAwareCameraCapabilities, config.live.controls.ptz)) {
const isOn =
view.context?.live?.ptzVisible === false
? false
+5 -26
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { afterAll, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import {
@@ -382,6 +383,7 @@ describe('ActionsManager.executeAction', () => {
expect.objectContaining({
viewName: 'live',
cameraID: 'camera',
failSafe: true,
}),
);
});
@@ -409,6 +411,7 @@ describe('ActionsManager.executeAction', () => {
expect.objectContaining({
viewName: 'timeline',
cameraID: 'camera',
failSafe: true,
}),
);
});
@@ -442,6 +445,7 @@ describe('ActionsManager.executeAction', () => {
expect.objectContaining({
viewName: 'clips',
cameraID: 'camera',
failSafe: true,
}),
);
});
@@ -467,6 +471,7 @@ describe('ActionsManager.executeAction', () => {
expect.objectContaining({
viewName: 'live',
cameraID: 'camera',
failSafe: true,
}),
);
});
@@ -503,32 +508,6 @@ describe('ActionsManager.executeAction', () => {
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
});
it('with an unsupported view', async () => {
const api = createCardAPI();
const manager = new ActionsManager(api);
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
view: 'timeline',
}),
);
await manager.executeFrigateAction(
createAction({
frigate_card_action: 'camera_select',
camera: 'camera',
})!,
);
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
expect.objectContaining({
// Should have fallen back to the default view.
viewName: 'live',
cameraID: 'camera',
}),
);
});
});
it('should handle live_substream_select action', async () => {
+23 -18
View File
@@ -7,6 +7,7 @@ import { EventMediaQueries } from '../../src/view/media-queries';
import { MediaQueriesResults } from '../../src/view/media-queries-results';
import { View } from '../../src/view/view';
import {
createAggregateCameraCapabilities,
createCameraCapabilities,
createCameraConfig,
createCameraManager,
@@ -15,7 +16,7 @@ import {
createHASS,
createStore,
createView,
generateViewMediaArray
generateViewMediaArray,
} from '../test-utils';
vi.mock('../../src/utils/media-to-view');
@@ -218,8 +219,8 @@ describe('ViewManager.setViewByParameters', () => {
},
]),
);
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(
createCameraCapabilities({
vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({
supportsClips: true,
}),
);
@@ -247,8 +248,8 @@ describe('ViewManager.setViewByParameters', () => {
},
]),
);
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(
createCameraCapabilities({
vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({
supportsClips: true,
}),
);
@@ -336,10 +337,12 @@ describe('ViewManager.setViewByParameters', () => {
]),
);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(createCameraCapabilities({
supportsSnapshots: false,
}));
vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({
supportsSnapshots: false,
}),
);
const manager = new ViewManager(api);
manager.setViewByParameters({
viewName: 'snapshots',
@@ -360,16 +363,18 @@ describe('ViewManager.setViewByParameters', () => {
]),
);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(createCameraCapabilities({
supportsSnapshots: false,
}));
vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({
supportsSnapshots: false,
}),
);
const manager = new ViewManager(api);
manager.setViewByParameters({
viewName: 'snapshots',
failSafe: true,
});
expect(manager.hasView()).toBeTruthy();
expect(manager.getView()?.view).toBe('live');
});
@@ -392,8 +397,8 @@ describe('ViewManager.setViewByParameters', () => {
},
]),
);
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(
createCameraCapabilities({
vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({
supportsClips: true,
supportsRecordings: true,
supportsSnapshots: true,
@@ -443,8 +448,8 @@ describe('ViewManager.setViewByParameters', () => {
},
]),
);
vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(
createCameraCapabilities({
vi.mocked(api.getCameraManager().getAggregateCameraCapabilities).mockReturnValue(
createAggregateCameraCapabilities({
supportsClips: true,
supportsRecordings: true,
supportsSnapshots: true,
File diff suppressed because it is too large Load Diff