feat: Add folder view to view folder media (#2077)

- Closes: #2068

Note: Renames existing `folder` view to `folders` [experimental
functionality]
This commit is contained in:
Dermot Duffy
2025-05-31 17:13:39 -07:00
committed by GitHub
parent 1901475065
commit 956a2e523c
30 changed files with 246 additions and 132 deletions
@@ -105,7 +105,17 @@ describe('ActionsManager', () => {
{
tap_action: {
action: 'navigate',
// Folder also uses the media gallery.
// Folders also uses the media viewer.
navigation_path: '4',
},
},
],
[
'folders' as const,
{
tap_action: {
action: 'navigate',
// Folders also uses the media gallery.
navigation_path: '3',
},
},
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
import { FolderAction } from '../../../../src/card-controller/actions/actions/folder';
import { FoldersViewAction } from '../../../../src/card-controller/actions/actions/folders-view';
import { FolderQuery } from '../../../../src/card-controller/folders/types';
import { FolderViewQuery } from '../../../../src/view/query';
import { createCardAPI, createFolder } from '../../../test-utils';
@@ -7,7 +7,7 @@ import { createCardAPI, createFolder } from '../../../test-utils';
describe('should handle folder action', async () => {
it('should handle folder action successfully', async () => {
const api = createCardAPI();
const action = new FolderAction(
const action = new FoldersViewAction(
{},
{
action: 'fire-dom-event',
@@ -20,7 +20,7 @@ describe('should handle folder action', async () => {
const query: FolderQuery = {
folder,
path: ['path'],
path: [{ ha: { id: 'path' } }],
};
vi.mocked(api.getFoldersManager().generateDefaultFolderQuery).mockReturnValue(query);
@@ -44,7 +44,7 @@ describe('should handle folder action', async () => {
it('should do nothing with non-existent folder', async () => {
const api = createCardAPI();
const action = new FolderAction(
const action = new FoldersViewAction(
{},
{
action: 'fire-dom-event',
@@ -64,7 +64,7 @@ describe('should handle folder action', async () => {
it('should do nothing with non-existent default query', async () => {
const api = createCardAPI();
const action = new FolderAction(
const action = new FoldersViewAction(
{},
{
action: 'fire-dom-event',
@@ -7,7 +7,7 @@ import { DefaultAction } from '../../../src/card-controller/actions/actions/defa
import { DisplayModeSelectAction } from '../../../src/card-controller/actions/actions/display-mode-select';
import { DownloadAction } from '../../../src/card-controller/actions/actions/download';
import { ExpandAction } from '../../../src/card-controller/actions/actions/expand';
import { FolderAction } from '../../../src/card-controller/actions/actions/folder';
import { FoldersViewAction } from '../../../src/card-controller/actions/actions/folders-view';
import { FullscreenAction } from '../../../src/card-controller/actions/actions/fullscreen';
import { InternalCallbackAction } from '../../../src/card-controller/actions/actions/internal-callback';
import { LogAction } from '../../../src/card-controller/actions/actions/log';
@@ -185,7 +185,8 @@ describe('ActionFactory', () => {
},
InternalCallbackAction,
],
[{ advanced_camera_card_action: 'folder' as const }, FolderAction],
[{ advanced_camera_card_action: 'folder' as const }, FoldersViewAction],
[{ advanced_camera_card_action: 'folders' as const }, FoldersViewAction],
])(
'advanced_camera_card_action: $advanced_camera_card_action',
(action: Partial<ActionConfig>, classObject: object) => {
@@ -1792,7 +1792,8 @@ describe('MenuButtonController', () => {
style: {},
type: 'custom:advanced-camera-card-menu-icon',
title: 'Folders',
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'folder' },
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'folders' },
hold_action: { action: 'fire-dom-event', advanced_camera_card_action: 'folder' },
});
});
@@ -1807,7 +1808,7 @@ describe('MenuButtonController', () => {
foldersManager,
view: createView({
view: 'folder',
query: new FolderViewQuery({ folder, path: [{ id: 'one' }] }),
query: new FolderViewQuery({ folder, path: [{ ha: { id: 'one' } }] }),
}),
});
@@ -1820,7 +1821,8 @@ describe('MenuButtonController', () => {
},
type: 'custom:advanced-camera-card-menu-icon',
title: 'Folders',
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'folder' },
tap_action: { action: 'fire-dom-event', advanced_camera_card_action: 'folders' },
hold_action: { action: 'fire-dom-event', advanced_camera_card_action: 'folder' },
});
});
@@ -1837,7 +1839,7 @@ describe('MenuButtonController', () => {
view: 'folder',
query: new FolderViewQuery({
folder: selectedFolder,
path: [{ id: 'id' }],
path: [{ ha: { id: 'id' } }],
}),
});
@@ -1858,18 +1860,28 @@ describe('MenuButtonController', () => {
items: [
{
enabled: true,
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-0',
},
icon: 'mdi:folder',
selected: false,
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
advanced_camera_card_action: 'folders',
folder: 'folder-0',
},
title: 'folder-0',
},
{
enabled: true,
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-selected',
},
icon: 'mdi:folder',
selected: true,
style: {
@@ -1877,7 +1889,7 @@ describe('MenuButtonController', () => {
},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
advanced_camera_card_action: 'folders',
folder: 'folder-selected',
},
title: 'folder-selected',
@@ -1909,23 +1921,33 @@ describe('MenuButtonController', () => {
{
enabled: true,
icon: 'mdi:folder',
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-0',
},
selected: false,
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
advanced_camera_card_action: 'folders',
folder: 'folder-0',
},
title: 'folder-0',
},
{
enabled: true,
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-1',
},
icon: 'mdi:folder',
selected: false,
style: {},
tap_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
advanced_camera_card_action: 'folders',
folder: 'folder-1',
},
title: 'folder-1',
+14 -9
View File
@@ -5,7 +5,7 @@ import { ActionConfig } from '../../src/config/schema/actions/types.js';
import {
createCameraAction,
createDisplayModeAction,
createFolderAction,
createFoldersViewAction,
createGeneralAction,
createInternalCallbackAction,
createLogAction,
@@ -64,14 +64,19 @@ describe('createCameraAction', () => {
});
describe('createFolderAction', () => {
it('should create folder action', () => {
expect(createFolderAction({ folderID: 'folderID', cardID: 'card_id' })).toEqual({
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
card_id: 'card_id',
folder: 'folderID',
});
});
it.each([['folder' as const], ['folders' as const]])(
'%s',
(viewName: 'folder' | 'folders') => {
expect(
createFoldersViewAction(viewName, { folderID: 'folderID', cardID: 'card_id' }),
).toEqual({
action: 'fire-dom-event',
advanced_camera_card_action: viewName,
card_id: 'card_id',
folder: 'folderID',
});
},
);
});
describe('createMediaPlayerAction', () => {
+27 -24
View File
@@ -11,31 +11,34 @@ import {
describe('getCameraIDsForViewName', () => {
describe('views that are always supported', () => {
it.each([['diagnostics' as const], ['image' as const], ['media' as const]])(
'%s',
(viewName: AdvancedCameraCardView) => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
},
{ cameraID: 'camera-2' },
]),
);
it.each([
['diagnostics' as const],
['folder' as const],
['folders' as const],
['image' as const],
['media' as const],
])('%s', (viewName: AdvancedCameraCardView) => {
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore).mockReturnValue(
createStore([
{
cameraID: 'camera-1',
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
},
{ cameraID: 'camera-2' },
]),
);
expect(getCameraIDsForViewName(cameraManager, viewName)).toEqual(
new Set(['camera-1', 'camera-2']),
);
expect(getCameraIDsForViewName(cameraManager, viewName, 'camera-1')).toEqual(
new Set(['camera-1', 'camera-2']),
);
expect(getCameraIDsForViewName(cameraManager, viewName, 'camera-2')).toEqual(
new Set(['camera-1', 'camera-2']),
);
},
);
expect(getCameraIDsForViewName(cameraManager, viewName)).toEqual(
new Set(['camera-1', 'camera-2']),
);
expect(getCameraIDsForViewName(cameraManager, viewName, 'camera-1')).toEqual(
new Set(['camera-1', 'camera-2']),
);
expect(getCameraIDsForViewName(cameraManager, viewName, 'camera-2')).toEqual(
new Set(['camera-1', 'camera-2']),
);
});
});
describe('views that respect dependencies and need a capability', () => {
+66 -41
View File
@@ -153,56 +153,81 @@ describe('View Basics', () => {
expect(view.context).toEqual({});
});
it('should detect gallery views', () => {
expect(createView({ view: 'clips' }).isMediaGalleryView()).toBeTruthy();
expect(createView({ view: 'folder' }).isMediaGalleryView()).toBeTruthy();
expect(createView({ view: 'snapshots' }).isMediaGalleryView()).toBeTruthy();
expect(createView({ view: 'recordings' }).isMediaGalleryView()).toBeTruthy();
describe('should detect folder views', () => {
it('should detect folder views', () => {
expect(createView({ view: 'folder' }).isAnyFolderView()).toBeTruthy();
expect(createView({ view: 'folders' }).isAnyFolderView()).toBeTruthy();
});
it('should not detect folder views', () => {
expect(createView({ view: 'live' }).isAnyFolderView()).toBeFalsy();
expect(createView({ view: 'clip' }).isAnyFolderView()).toBeFalsy();
expect(createView({ view: 'clips' }).isAnyFolderView()).toBeFalsy();
});
});
it('should not detect gallery view', () => {
expect(createView({ view: 'live' }).isMediaGalleryView()).toBeFalsy();
expect(createView({ view: 'timeline' }).isMediaGalleryView()).toBeFalsy();
describe('should detect media views', () => {
it('should detect any media views', () => {
expect(createView({ view: 'clip' }).isAnyMediaView()).toBeTruthy();
expect(createView({ view: 'snapshot' }).isAnyMediaView()).toBeTruthy();
expect(createView({ view: 'media' }).isAnyMediaView()).toBeTruthy();
expect(createView({ view: 'recording' }).isAnyMediaView()).toBeTruthy();
expect(createView({ view: 'live' }).isAnyMediaView()).toBeTruthy();
expect(createView({ view: 'image' }).isAnyMediaView()).toBeTruthy();
expect(createView({ view: 'folder' }).isAnyMediaView()).toBeTruthy();
});
it('should not detect any media view', () => {
expect(createView({ view: 'timeline' }).isAnyMediaView()).toBeFalsy();
});
});
it('should detect any media views', () => {
expect(createView({ view: 'clip' }).isAnyMediaView()).toBeTruthy();
expect(createView({ view: 'snapshot' }).isAnyMediaView()).toBeTruthy();
expect(createView({ view: 'media' }).isAnyMediaView()).toBeTruthy();
expect(createView({ view: 'recording' }).isAnyMediaView()).toBeTruthy();
expect(createView({ view: 'live' }).isAnyMediaView()).toBeTruthy();
expect(createView({ view: 'image' }).isAnyMediaView()).toBeTruthy();
describe('should detect gallery views', () => {
it('should detect gallery views', () => {
expect(createView({ view: 'clips' }).isMediaGalleryView()).toBeTruthy();
expect(createView({ view: 'folders' }).isMediaGalleryView()).toBeTruthy();
expect(createView({ view: 'snapshots' }).isMediaGalleryView()).toBeTruthy();
expect(createView({ view: 'recordings' }).isMediaGalleryView()).toBeTruthy();
});
it('should not detect gallery view', () => {
expect(createView({ view: 'live' }).isMediaGalleryView()).toBeFalsy();
expect(createView({ view: 'timeline' }).isMediaGalleryView()).toBeFalsy();
});
});
it('should not detect any media view', () => {
expect(createView({ view: 'timeline' }).isAnyMediaView()).toBeFalsy();
describe('should detect viewer views', () => {
it('should detect viewer views', () => {
expect(createView({ view: 'clip' }).isViewerView()).toBeTruthy();
expect(createView({ view: 'snapshot' }).isViewerView()).toBeTruthy();
expect(createView({ view: 'media' }).isViewerView()).toBeTruthy();
expect(createView({ view: 'recording' }).isViewerView()).toBeTruthy();
expect(createView({ view: 'folder' }).isAnyMediaView()).toBeTruthy();
});
it('should not detect viewer views', () => {
expect(createView({ view: 'live' }).isViewerView()).toBeFalsy();
expect(createView({ view: 'live' }).isViewerView()).toBeFalsy();
expect(createView({ view: 'timeline' }).isViewerView()).toBeFalsy();
});
});
it('should detect viewer views', () => {
expect(createView({ view: 'clip' }).isViewerView()).toBeTruthy();
expect(createView({ view: 'snapshot' }).isViewerView()).toBeTruthy();
expect(createView({ view: 'media' }).isViewerView()).toBeTruthy();
expect(createView({ view: 'recording' }).isViewerView()).toBeTruthy();
});
describe('should get default media type', () => {
it('should get default media type', () => {
expect(createView({ view: 'clip' }).getDefaultMediaType()).toBe('clips');
expect(createView({ view: 'clips' }).getDefaultMediaType()).toBe('clips');
expect(createView({ view: 'snapshot' }).getDefaultMediaType()).toBe('snapshots');
expect(createView({ view: 'snapshots' }).getDefaultMediaType()).toBe('snapshots');
expect(createView({ view: 'recording' }).getDefaultMediaType()).toBe('recordings');
expect(createView({ view: 'recordings' }).getDefaultMediaType()).toBe(
'recordings',
);
});
it('should not detect viewer views', () => {
expect(createView({ view: 'live' }).isViewerView()).toBeFalsy();
expect(createView({ view: 'live' }).isViewerView()).toBeFalsy();
expect(createView({ view: 'timeline' }).isViewerView()).toBeFalsy();
});
it('should get default media type', () => {
expect(createView({ view: 'clip' }).getDefaultMediaType()).toBe('clips');
expect(createView({ view: 'clips' }).getDefaultMediaType()).toBe('clips');
expect(createView({ view: 'snapshot' }).getDefaultMediaType()).toBe('snapshots');
expect(createView({ view: 'snapshots' }).getDefaultMediaType()).toBe('snapshots');
expect(createView({ view: 'recording' }).getDefaultMediaType()).toBe('recordings');
expect(createView({ view: 'recordings' }).getDefaultMediaType()).toBe('recordings');
});
it('should not get default media type', () => {
expect(createView({ view: 'live' }).getDefaultMediaType()).toBeNull();
expect(createView({ view: 'timeline' }).getDefaultMediaType()).toBeNull();
it('should not get default media type', () => {
expect(createView({ view: 'live' }).getDefaultMediaType()).toBeNull();
expect(createView({ view: 'timeline' }).getDefaultMediaType()).toBeNull();
});
});
it('should determine if display mode is grid', () => {