feat: Implement basic general folder support (#2051)

- Related: #1748
This commit is contained in:
Dermot Duffy
2025-05-21 19:59:21 -07:00
committed by GitHub
parent 2eb0d9e35e
commit c6a4c8aea2
350 changed files with 12837 additions and 4509 deletions
@@ -1,9 +1,10 @@
import isEqual from 'lodash-es/isEqual';
import { isEqual } from 'lodash-es';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { Capabilities } from '../../src/camera-manager/capabilities.js';
import { CameraManager } from '../../src/camera-manager/manager.js';
import { CameraManagerCameraMetadata } from '../../src/camera-manager/types.js';
import { FoldersManager } from '../../src/card-controller/folders/manager.js';
import { FullscreenManager } from '../../src/card-controller/fullscreen/fullscreen-manager.js';
import { MediaPlayerManager } from '../../src/card-controller/media-player-manager.js';
import { MicrophoneManager } from '../../src/card-controller/microphone-manager.js';
@@ -17,16 +18,18 @@ import { ViewDisplayMode } from '../../src/config/schema/common/display.js';
import { MenuItem } from '../../src/config/schema/elements/custom/menu/types.js';
import { AdvancedCameraCardConfig } from '../../src/config/schema/types.js';
import { HomeAssistant } from '../../src/ha/types.js';
import { MediaPlayerController } from '../../src/types.js';
import { MediaPlayerController, PTZMovementType } from '../../src/types.js';
import { createGeneralAction, createViewAction } from '../../src/utils/action.js';
import { MediaQueriesResults } from '../../src/view/media-queries-results.js';
import { ViewMedia } from '../../src/view/media.js';
import { ViewMedia, ViewMediaType } from '../../src/view/item.js';
import { QueryResults } from '../../src/view/query-results.js';
import { FolderViewQuery } from '../../src/view/query.js';
import { View } from '../../src/view/view.js';
import {
createCameraConfig,
createCameraManager,
createCapabilities,
createConfig,
createFolder,
createHASS,
createMediaCapabilities,
createMediaLoadedInfo,
@@ -858,8 +861,12 @@ describe('MenuButtonController', () => {
);
const view = createView({
view: 'media',
queryResults: new MediaQueriesResults({
results: [new ViewMedia('clip', 'camera-1')],
queryResults: new QueryResults({
results: [
new ViewMedia(ViewMediaType.Clip, {
cameraID: 'camera-1',
}),
],
selectedIndex: 0,
}),
});
@@ -893,8 +900,8 @@ describe('MenuButtonController', () => {
createMediaCapabilities({ canDownload: true }),
);
const view = createView({
queryResults: new MediaQueriesResults({
results: [new ViewMedia('clip', 'camera-1')],
queryResults: new QueryResults({
results: [new ViewMedia(ViewMediaType.Clip, { cameraID: 'camera-1' })],
selectedIndex: 0,
}),
});
@@ -915,8 +922,8 @@ describe('MenuButtonController', () => {
);
const view = createView({
view: 'live',
queryResults: new MediaQueriesResults({
results: [new ViewMedia('clip', 'camera-1')],
queryResults: new QueryResults({
results: [new ViewMedia(ViewMediaType.Clip, { cameraID: 'camera-1' })],
selectedIndex: 0,
}),
});
@@ -1499,7 +1506,7 @@ describe('MenuButtonController', () => {
const store = createStore([
{
cameraID: 'camera-1',
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
},
]);
@@ -1519,7 +1526,7 @@ describe('MenuButtonController', () => {
const store = createStore([
{
cameraID: 'camera-1',
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
},
]);
@@ -1539,7 +1546,7 @@ describe('MenuButtonController', () => {
const store = createStore([
{
cameraID: 'camera-1',
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
},
]);
@@ -1573,7 +1580,7 @@ describe('MenuButtonController', () => {
const store = createStore([
{
cameraID: 'camera-1',
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
},
]);
@@ -1605,7 +1612,7 @@ describe('MenuButtonController', () => {
const store = createStore([
{
cameraID: 'camera-1',
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
},
]);
@@ -1643,7 +1650,7 @@ describe('MenuButtonController', () => {
},
{
cameraID: 'camera-2',
capabilities: new Capabilities({ ptz: { left: ['relative'] } }),
capabilities: new Capabilities({ ptz: { left: [PTZMovementType.Relative] } }),
},
]);
const view = createView({
@@ -1701,7 +1708,7 @@ describe('MenuButtonController', () => {
const view = createView({
view: viewName,
camera: 'camera-1',
queryResults: new MediaQueriesResults({
queryResults: new QueryResults({
results: [new TestViewMedia({ id: 'media-1' })],
selectedIndex: 0,
}),
@@ -1750,6 +1757,184 @@ describe('MenuButtonController', () => {
);
});
describe('should have folders button', () => {
it('should have no folders button without folders', () => {
const foldersManager = mock<FoldersManager>();
foldersManager.getFolders.mockReturnValue(new Map().entries());
const buttons = calculateButtons(controller, {
foldersManager,
});
expect(buttons).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
icon: 'mdi:folder',
}),
]),
);
});
it('should have a folders button for a single folder outside the folder view', () => {
const foldersManager = mock<FoldersManager>();
foldersManager.getFolders.mockReturnValue(
new Map([['folder-0', createFolder({ id: 'folder-0' })]]).entries(),
);
const buttons = calculateButtons(controller, {
foldersManager,
});
expect(buttons).toContainEqual({
icon: 'mdi:folder',
enabled: true,
priority: 50,
style: {},
type: 'custom:advanced-camera-card-menu-icon',
title: 'Folders',
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'folder' },
});
});
it('should have a folders button for a single folder inside the folder view', () => {
const folder = createFolder({ id: 'folder-0' });
const foldersManager = mock<FoldersManager>();
foldersManager.getFolders.mockReturnValue(
new Map([['folder-0', folder]]).entries(),
);
const buttons = calculateButtons(controller, {
foldersManager,
view: createView({
view: 'folder',
query: new FolderViewQuery({ folder, path: [{ id: 'one' }] }),
}),
});
expect(buttons).toContainEqual({
icon: 'mdi:folder',
enabled: true,
priority: 50,
style: {
color: 'var(--advanced-camera-card-menu-button-active-color)',
},
type: 'custom:advanced-camera-card-menu-icon',
title: 'Folders',
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'folder' },
});
});
it('should have a folders submenu for multiple folder with a selected folder', () => {
const foldersManager = mock<FoldersManager>();
const selectedFolder = createFolder({ id: 'folder-selected' });
const folders = new Map([
['folder-0', createFolder({ id: 'folder-0' })],
['folder-selected', selectedFolder],
]);
foldersManager.getFolders.mockReturnValue(folders.entries());
const view = createView({
view: 'folder',
query: new FolderViewQuery({
folder: selectedFolder,
path: [{ id: 'id' }],
}),
});
const buttons = calculateButtons(controller, {
foldersManager,
view,
});
expect(buttons).toContainEqual({
icon: 'mdi:folder-multiple',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-submenu',
title: 'Folders',
style: {
color: 'var(--advanced-camera-card-menu-button-active-color)',
},
items: [
{
enabled: true,
icon: 'mdi:folder',
selected: false,
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-0',
},
title: 'folder-0',
},
{
enabled: true,
icon: 'mdi:folder',
selected: true,
style: {
color: 'var(--advanced-camera-card-menu-button-active-color)',
},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-selected',
},
title: 'folder-selected',
},
],
});
});
it('should have a folders submenu for multiple folder without a selected folder', () => {
const foldersManager = mock<FoldersManager>();
const folders = new Map([
['folder-0', createFolder({ id: 'folder-0' })],
['folder-1', createFolder({ id: 'folder-1' })],
]);
foldersManager.getFolders.mockReturnValue(folders.entries());
const buttons = calculateButtons(controller, {
foldersManager,
});
expect(buttons).toContainEqual({
icon: 'mdi:folder-multiple',
enabled: true,
priority: 50,
type: 'custom:advanced-camera-card-menu-submenu',
title: 'Folders',
style: {},
items: [
{
enabled: true,
icon: 'mdi:folder',
selected: false,
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-0',
},
title: 'folder-0',
},
{
enabled: true,
icon: 'mdi:folder',
selected: false,
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-1',
},
title: 'folder-1',
},
],
});
});
});
describe('should handle dynamic buttons', () => {
it('successfully', () => {
const button: MenuItem = {