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
+29 -1
View File
@@ -114,7 +114,7 @@ advanced_camera_card_action: expand
## `folder` ## `folder`
Show a given folder in the folder gallery. Show media from a given folder in the media viewer.
```yaml ```yaml
action: custom:advanced-camera-card-action action: custom:advanced-camera-card-action
@@ -126,6 +126,20 @@ advanced_camera_card_action: folder
| --------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | | --------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `folder` | The first configured folder (under [`folders`](../../folders.md)). | An optional id of the folder to show, see the `id` parameter under [`folders` configuration](../../folders.md). | | `folder` | The first configured folder (under [`folders`](../../folders.md)). | An optional id of the folder to show, see the `id` parameter under [`folders` configuration](../../folders.md). |
## `folders`
Show a given folder in the folders gallery.
```yaml
action: custom:advanced-camera-card-action
advanced_camera_card_action: folders
# [...]
```
| Parameter | Default | Description |
| --------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `folder` | The first configured folder (under [`folders`](../../folders.md)). | An optional id of the folder to show, see the `id` parameter under [`folders` configuration](../../folders.md). |
## `fullscreen` ## `fullscreen`
Toggle fullscreen. Toggle fullscreen.
@@ -821,4 +835,18 @@ elements:
- action: custom:advanced-camera-card-action - action: custom:advanced-camera-card-action
advanced_camera_card_action: status_bar advanced_camera_card_action: status_bar
status_bar_action: reset status_bar_action: reset
- type: custom:advanced-camera-card-menu-icon
icon: mdi:alpha-m-circle-outline
title: View Folder Media
tap_action:
- action: custom:advanced-camera-card-action
advanced_camera_card_action: folder
folder: my-folder
- type: custom:advanced-camera-card-menu-icon
icon: mdi:alpha-n-circle-outline
title: View Folders Gallery
tap_action:
- action: custom:advanced-camera-card-action
advanced_camera_card_action: folders
folder: my-folder
``` ```
+1 -1
View File
@@ -36,7 +36,7 @@ menu:
| `display_mode` | The `display_mode` button allows changing between single and grid views. | | `display_mode` | The `display_mode` button allows changing between single and grid views. |
| `download` | The `download` menu button: allow direct download of the media being displayed. | | `download` | The `download` menu button: allow direct download of the media being displayed. |
| `expand` | The `expand` menu button: expand the card into a popup/dialog. | | `expand` | The `expand` menu button: expand the card into a popup/dialog. |
| `folders` | The `folders` menu button to select a folder of media to view in the [`media_gallery`](./media-gallery.md). Will only appear if [`folders`](./folders.md) are configured. | | `folders` | The `folders` menu button: brings the user to a [gallery](./media-gallery.md) of folders on tap and to the media viewer with media from the folder on hold. Will only appear if [`folders`](./folders.md) are configured. |
| `fullscreen` | The `fullscreen` menu button: expand the card to consume the fullscreen. Please note that fullscreen behavior on iPhone is limited, see [troubleshooting](../troubleshooting.md?id=fullscreen-doesn39t-work-on-iphone). | | `fullscreen` | The `fullscreen` menu button: expand the card to consume the fullscreen. Please note that fullscreen behavior on iPhone is limited, see [troubleshooting](../troubleshooting.md?id=fullscreen-doesn39t-work-on-iphone). |
| `image` | The `image` view menu button: brings the user to the static `image` view. | | `image` | The `image` view menu button: brings the user to the static `image` view. |
| `iris` | The main Advanced Camera Card `iris` menu button: brings the user to the default configured view (`view.default`), or collapses/expands the menu if the `menu.style` is `hidden` . | | `iris` | The main Advanced Camera Card `iris` menu button: brings the user to the default configured view (`view.default`), or collapses/expands the menu if the `menu.style` is `hidden` . |
+2 -1
View File
@@ -142,7 +142,8 @@ This card supports several different views.
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clip` | Shows a viewer for the most recent clip for this camera. Can also be accessed by holding down the `clips` menu icon. | | `clip` | Shows a viewer for the most recent clip for this camera. Can also be accessed by holding down the `clips` menu icon. |
| `clips` | Shows a gallery of clips for this camera. | | `clips` | Shows a gallery of clips for this camera. |
| `folder` | Shows a gallery of media from a [`folder`](./folders.md). | | `folder` | Shows a viewer for the media from the default [`folder`](./folders.md). |
| `folders` | Shows a gallery of media and subfolders from the default [`folder`](./folders.md). |
| `image` | Shows a static image specified by the `image` parameter, can be used as a discrete default view or a screensaver (via `view.interaction_seconds`). | | `image` | Shows a static image specified by the `image` parameter, can be used as a discrete default view or a screensaver (via `view.interaction_seconds`). |
| `live` | Shows the live camera view with the configured [live provider](./cameras/live-provider.md). | | `live` | Shows the live camera view with the configured [live provider](./cameras/live-provider.md). |
| `recording` | Shows a viewer for the most recent recording for this camera. Can also be accessed by holding down the `recordings` menu icon. | | `recording` | Shows a viewer for the most recent recording for this camera. Can also be accessed by holding down the `recordings` menu icon. |
@@ -1,9 +1,9 @@
import { FolderActionConfig } from '../../../config/schema/actions/custom/folder'; import { FoldersViewActionConfig } from '../../../config/schema/actions/custom/folders-view';
import { FolderViewQuery } from '../../../view/query'; import { FolderViewQuery } from '../../../view/query';
import { CardActionsAPI } from '../../types'; import { CardActionsAPI } from '../../types';
import { AdvancedCameraCardAction } from './base'; import { AdvancedCameraCardAction } from './base';
export class FolderAction extends AdvancedCameraCardAction<FolderActionConfig> { export class FoldersViewAction extends AdvancedCameraCardAction<FoldersViewActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> { public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api); await super.execute(api);
@@ -19,7 +19,8 @@ export class FolderAction extends AdvancedCameraCardAction<FolderActionConfig> {
await api.getViewManager().setViewByParametersWithExistingQuery({ await api.getViewManager().setViewByParametersWithExistingQuery({
params: { params: {
view: 'folder', // Supports both 'folder' and 'folders' views.
view: this._action.advanced_camera_card_action,
query: new FolderViewQuery(query), query: new FolderViewQuery(query),
}, },
}); });
+3 -2
View File
@@ -10,7 +10,7 @@ import { DefaultAction } from './actions/default';
import { DisplayModeSelectAction } from './actions/display-mode-select'; import { DisplayModeSelectAction } from './actions/display-mode-select';
import { DownloadAction } from './actions/download'; import { DownloadAction } from './actions/download';
import { ExpandAction } from './actions/expand'; import { ExpandAction } from './actions/expand';
import { FolderAction } from './actions/folder'; import { FoldersViewAction } from './actions/folders-view';
import { FullscreenAction } from './actions/fullscreen'; import { FullscreenAction } from './actions/fullscreen';
import { InternalCallbackAction } from './actions/internal-callback'; import { InternalCallbackAction } from './actions/internal-callback';
import { LogAction } from './actions/log'; import { LogAction } from './actions/log';
@@ -152,7 +152,8 @@ export class ActionFactory {
case INTERNAL_CALLBACK_ACTION: case INTERNAL_CALLBACK_ACTION:
return new InternalCallbackAction(context, action, options?.config); return new InternalCallbackAction(context, action, options?.config);
case 'folder': case 'folder':
return new FolderAction(context, action, options?.config); case 'folders':
return new FoldersViewAction(context, action, options?.config);
} }
/* istanbul ignore next: this path cannot be reached -- @preserve */ /* istanbul ignore next: this path cannot be reached -- @preserve */
@@ -120,6 +120,7 @@ export class ViewQueryExecutor {
viewModifiers.push(...(await executeMediaQuery(mediaType))); viewModifiers.push(...(await executeMediaQuery(mediaType)));
break; break;
case 'folder': case 'folder':
case 'folders':
viewModifiers.push(...(await executeFolderQuery())); viewModifiers.push(...(await executeFolderQuery()));
break; break;
} }
+6 -5
View File
@@ -15,7 +15,7 @@ import { MediaLoadedInfo } from '../types';
import { import {
createCameraAction, createCameraAction,
createDisplayModeAction, createDisplayModeAction,
createFolderAction, createFoldersViewAction,
createGeneralAction, createGeneralAction,
createMediaPlayerAction, createMediaPlayerAction,
createPTZControlsAction, createPTZControlsAction,
@@ -658,7 +658,8 @@ export class MenuButtonController {
type: 'custom:advanced-camera-card-menu-icon', type: 'custom:advanced-camera-card-menu-icon',
title: folder.title ?? localize('config.menu.buttons.folders'), title: folder.title ?? localize('config.menu.buttons.folders'),
style: isSelected ? this._getEmphasizedStyle() : {}, style: isSelected ? this._getEmphasizedStyle() : {},
tap_action: createFolderAction(), tap_action: createFoldersViewAction('folders'),
hold_action: createFoldersViewAction('folder'),
}; };
} }
@@ -667,14 +668,14 @@ export class MenuButtonController {
QueryClassifier.isFolderQuery(view?.query) && QueryClassifier.isFolderQuery(view?.query) &&
view.query.getQuery()?.folder.id === id; view.query.getQuery()?.folder.id === id;
const action = createFolderAction({ folderID: id });
return { return {
enabled: true, enabled: true,
title: folder.title ?? folder.id, title: folder.title ?? folder.id,
icon: folder.icon ?? 'mdi:folder', icon: folder.icon ?? 'mdi:folder',
selected: isSelected, selected: isSelected,
style: isSelected ? this._getEmphasizedStyle() : {}, style: isSelected ? this._getEmphasizedStyle() : {},
...(action && { tap_action: action }), tap_action: createFoldersViewAction('folders', { folderID: id }),
hold_action: createFoldersViewAction('folder', { folderID: id }),
}; };
}); });
@@ -684,7 +685,7 @@ export class MenuButtonController {
type: 'custom:advanced-camera-card-menu-submenu', type: 'custom:advanced-camera-card-menu-submenu',
title: localize('config.menu.buttons.folders'), title: localize('config.menu.buttons.folders'),
items: submenuItems, items: submenuItems,
style: view?.is('folder') ? this._getEmphasizedStyle() : {}, style: view?.isAnyFolderView() ? this._getEmphasizedStyle() : {},
}; };
} }
+4 -4
View File
@@ -68,7 +68,7 @@ export class AdvancedCameraCardViews extends LitElement {
if (view?.is('live') || this._shouldLivePreload()) { if (view?.is('live') || this._shouldLivePreload()) {
import('./live/index.js'); import('./live/index.js');
} }
if (view?.isMediaGalleryView() && !view.is('folder')) { if (view?.isMediaGalleryView() && !view.is('folders')) {
import('./gallery/media-gallery.js'); import('./gallery/media-gallery.js');
} else if (view?.isViewerView()) { } else if (view?.isViewerView()) {
import('./viewer/index.js'); import('./viewer/index.js');
@@ -76,7 +76,7 @@ export class AdvancedCameraCardViews extends LitElement {
import('./image.js'); import('./image.js');
} else if (view?.is('timeline')) { } else if (view?.is('timeline')) {
import('./timeline.js'); import('./timeline.js');
} else if (view?.is('folder')) { } else if (view?.is('folders')) {
import('./gallery/folder-gallery.js'); import('./gallery/folder-gallery.js');
} }
} }
@@ -157,7 +157,7 @@ export class AdvancedCameraCardViews extends LitElement {
> >
</advanced-camera-card-image>` </advanced-camera-card-image>`
: ``} : ``}
${!this.hide && view?.isMediaGalleryView() && !view.is('folder') ${!this.hide && view?.isMediaGalleryView() && !view.is('folders')
? html` <advanced-camera-card-media-gallery ? html` <advanced-camera-card-media-gallery
.hass=${this.hass} .hass=${this.hass}
.viewManagerEpoch=${this.viewManagerEpoch} .viewManagerEpoch=${this.viewManagerEpoch}
@@ -200,7 +200,7 @@ export class AdvancedCameraCardViews extends LitElement {
> >
</advanced-camera-card-diagnostics>` </advanced-camera-card-diagnostics>`
: ``} : ``}
${!this.hide && view?.is('folder') ${!this.hide && view?.is('folders')
? html` <advanced-camera-card-folder-gallery ? html` <advanced-camera-card-folder-gallery
.hass=${this.hass} .hass=${this.hass}
.viewManagerEpoch=${this.viewManagerEpoch} .viewManagerEpoch=${this.viewManagerEpoch}
@@ -1,10 +0,0 @@
import { z } from 'zod';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
export const folderActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend(
{
advanced_camera_card_action: z.literal('folder'),
folder: z.string().optional(),
},
);
export type FolderActionConfig = z.infer<typeof folderActionConfigSchema>;
@@ -0,0 +1,9 @@
import { z } from 'zod';
import { advancedCameraCardCustomActionsBaseSchema } from './base';
export const foldersViewActionConfigSchema =
advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('folders').or(z.literal('folder')),
folder: z.string().optional(),
});
export type FoldersViewActionConfig = z.infer<typeof foldersViewActionConfigSchema>;
+3 -3
View File
@@ -7,14 +7,14 @@ import { advancedCameraCardCustomActionsBaseSchema } from './base';
type AdvancedCameraCardUserSpecifiedViewWithoutFolder = Exclude< type AdvancedCameraCardUserSpecifiedViewWithoutFolder = Exclude<
AdvancedCameraCardUserSpecifiedView, AdvancedCameraCardUserSpecifiedView,
'folder' 'folder' | 'folders'
>; >;
export const viewActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({ export const viewActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.enum( advanced_camera_card_action: z.enum(
// The folder view is handled by the `folder` action since it accepts an // The folder/folders views are handled separately as they accept an
// optional folder ID. // optional folder ID.
VIEWS_USER_SPECIFIED.filter((view) => view !== 'folder') as [ VIEWS_USER_SPECIFIED.filter((view) => view !== 'folder' && view !== 'folders') as [
AdvancedCameraCardUserSpecifiedViewWithoutFolder, AdvancedCameraCardUserSpecifiedViewWithoutFolder,
...AdvancedCameraCardUserSpecifiedViewWithoutFolder[], ...AdvancedCameraCardUserSpecifiedViewWithoutFolder[],
], ],
+2 -2
View File
@@ -3,7 +3,7 @@ import { statusBarItemBaseSchema } from '../common/status-bar';
import { advancedCameraCardCustomActionsBaseSchema } from './custom/base'; import { advancedCameraCardCustomActionsBaseSchema } from './custom/base';
import { cameraSelectActionConfigSchema } from './custom/camera-select'; import { cameraSelectActionConfigSchema } from './custom/camera-select';
import { viewDisplayModeActionConfigSchema } from './custom/display-mode'; import { viewDisplayModeActionConfigSchema } from './custom/display-mode';
import { folderActionConfigSchema } from './custom/folder'; import { foldersViewActionConfigSchema } from './custom/folders-view';
import { generalActionConfigSchema } from './custom/general'; import { generalActionConfigSchema } from './custom/general';
import { internalCallbackActionConfigSchema } from './custom/internal'; import { internalCallbackActionConfigSchema } from './custom/internal';
import { logActionConfigSchema } from './custom/log'; import { logActionConfigSchema } from './custom/log';
@@ -42,7 +42,7 @@ export const statusBarActionConfigSchema: z.ZodSchema<
const advancedCameraCardCustomActionSchema = z.union([ const advancedCameraCardCustomActionSchema = z.union([
cameraSelectActionConfigSchema, cameraSelectActionConfigSchema,
folderActionConfigSchema, foldersViewActionConfigSchema,
generalActionConfigSchema, generalActionConfigSchema,
internalCallbackActionConfigSchema, internalCallbackActionConfigSchema,
logActionConfigSchema, logActionConfigSchema,
+1
View File
@@ -17,6 +17,7 @@ export const VIEWS_USER_SPECIFIED = [
'clip', 'clip',
'clips', 'clips',
'folder', 'folder',
'folders',
'snapshot', 'snapshot',
'snapshots', 'snapshots',
'recording', 'recording',
+1
View File
@@ -414,6 +414,7 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
{ value: 'clip', label: localize('config.view.views.clip') }, { value: 'clip', label: localize('config.view.views.clip') },
{ value: 'clips', label: localize('config.view.views.clips') }, { value: 'clips', label: localize('config.view.views.clips') },
{ value: 'folder', label: localize('config.view.views.folder') }, { value: 'folder', label: localize('config.view.views.folder') },
{ value: 'folders', label: localize('config.view.views.folders') },
{ value: 'image', label: localize('config.view.views.image') }, { value: 'image', label: localize('config.view.views.image') },
{ value: 'live', label: localize('config.view.views.live') }, { value: 'live', label: localize('config.view.views.live') },
{ value: 'recording', label: localize('config.view.views.recording') }, { value: 'recording', label: localize('config.view.views.recording') },
+1
View File
@@ -589,6 +589,7 @@
"clips": "Galeria de clips", "clips": "Galeria de clips",
"current": "Vista actual", "current": "Vista actual",
"folder": "", "folder": "",
"folders": "",
"image": "Imatge estàtica", "image": "Imatge estàtica",
"live": "Visualització en directe", "live": "Visualització en directe",
"recording": "Enregistrament més recent", "recording": "Enregistrament més recent",
+2 -1
View File
@@ -588,7 +588,8 @@
"clip": "Most recent clip", "clip": "Most recent clip",
"clips": "Clips gallery", "clips": "Clips gallery",
"current": "Current view", "current": "Current view",
"folder": "Folder view", "folder": "Folder media",
"folders": "Folders gallery",
"image": "Static image", "image": "Static image",
"live": "Live view", "live": "Live view",
"recording": "Most recent recording", "recording": "Most recent recording",
+1
View File
@@ -589,6 +589,7 @@
"clips": "Galerie de clips", "clips": "Galerie de clips",
"current": "Vue actuelle", "current": "Vue actuelle",
"folder": "", "folder": "",
"folders": "",
"image": "Image statique", "image": "Image statique",
"live": "Vue en direct", "live": "Vue en direct",
"recording": "Enregistrement le plus récent", "recording": "Enregistrement le plus récent",
+1
View File
@@ -589,6 +589,7 @@
"clips": "Galleria delle clip", "clips": "Galleria delle clip",
"current": "Vista corrente", "current": "Vista corrente",
"folder": "", "folder": "",
"folders": "",
"image": "Immagine statica", "image": "Immagine statica",
"live": "Dal vivo", "live": "Dal vivo",
"recording": "", "recording": "",
+1
View File
@@ -589,6 +589,7 @@
"clips": "Galeria de clipes", "clips": "Galeria de clipes",
"current": "Visualização atual", "current": "Visualização atual",
"folder": "", "folder": "",
"folders": "",
"image": "Imagem estática", "image": "Imagem estática",
"live": "Visualização ao vivo", "live": "Visualização ao vivo",
"recording": "Gravação mais recente", "recording": "Gravação mais recente",
+1
View File
@@ -589,6 +589,7 @@
"clips": "Galeria de clipes", "clips": "Galeria de clipes",
"current": "Visualização atual", "current": "Visualização atual",
"folder": "", "folder": "",
"folders": "",
"image": "Imagem estática", "image": "Imagem estática",
"live": "Visualização ao vivo", "live": "Visualização ao vivo",
"recording": "", "recording": "",
+10 -7
View File
@@ -2,7 +2,7 @@ import { CardActionsAPI } from '../card-controller/types.js';
import { ZoomSettingsBase } from '../components-lib/zoom/types.js'; import { ZoomSettingsBase } from '../components-lib/zoom/types.js';
import { CameraSelectActionConfig } from '../config/schema/actions/custom/camera-select.js'; import { CameraSelectActionConfig } from '../config/schema/actions/custom/camera-select.js';
import { DisplayModeActionConfig } from '../config/schema/actions/custom/display-mode.js'; import { DisplayModeActionConfig } from '../config/schema/actions/custom/display-mode.js';
import { FolderActionConfig } from '../config/schema/actions/custom/folder.js'; import { FoldersViewActionConfig } from '../config/schema/actions/custom/folders-view.js';
import { import {
AdvancedCameraCardGeneralAction, AdvancedCameraCardGeneralAction,
GeneralActionConfig, GeneralActionConfig,
@@ -47,7 +47,7 @@ export function createGeneralAction(
} }
export function createViewAction( export function createViewAction(
action: Exclude<AdvancedCameraCardUserSpecifiedView, 'folder'>, action: Exclude<AdvancedCameraCardUserSpecifiedView, 'folder' | 'folders'>,
options?: { options?: {
cardID?: string; cardID?: string;
}, },
@@ -74,13 +74,16 @@ export function createCameraAction(
}; };
} }
export function createFolderAction(options?: { export function createFoldersViewAction(
cardID?: string; view: 'folder' | 'folders',
folderID?: string; options?: {
}): FolderActionConfig { cardID?: string;
folderID?: string;
},
): FoldersViewActionConfig {
return { return {
action: 'fire-dom-event', action: 'fire-dom-event',
advanced_camera_card_action: 'folder', advanced_camera_card_action: view,
...(options?.folderID && { folder: options.folderID }), ...(options?.folderID && { folder: options.folderID }),
...(options?.cardID && { card_id: options.cardID }), ...(options?.cardID && { card_id: options.cardID }),
}; };
+1
View File
@@ -19,6 +19,7 @@ export const getCameraIDsForViewName = (
case 'diagnostics': case 'diagnostics':
case 'image': case 'image':
case 'folder': case 'folder':
case 'folders':
case 'media': case 'media':
return cameraManager.getStore().getCameraIDs(); return cameraManager.getStore().getCameraIDs();
+6 -2
View File
@@ -126,7 +126,7 @@ export class View {
* Determine if a view is a media gallery. * Determine if a view is a media gallery.
*/ */
public isMediaGalleryView(): boolean { public isMediaGalleryView(): boolean {
return ['clips', 'folder', 'snapshots', 'recordings'].includes(this.view); return ['clips', 'folders', 'snapshots', 'recordings'].includes(this.view);
} }
/** /**
@@ -137,11 +137,15 @@ export class View {
return this.isViewerView() || this.is('live') || this.is('image'); return this.isViewerView() || this.is('live') || this.is('image');
} }
public isAnyFolderView(): boolean {
return ['folder', 'folders'].includes(this.view);
}
/** /**
* Determine if a view is for the media viewer. * Determine if a view is for the media viewer.
*/ */
public isViewerView(): boolean { public isViewerView(): boolean {
return ['media', 'clip', 'snapshot', 'recording'].includes(this.view); return ['folder', 'media', 'clip', 'snapshot', 'recording'].includes(this.view);
} }
public supportsMultipleDisplayModes(): boolean { public supportsMultipleDisplayModes(): boolean {
@@ -105,7 +105,17 @@ describe('ActionsManager', () => {
{ {
tap_action: { tap_action: {
action: 'navigate', 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', navigation_path: '3',
}, },
}, },
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'; 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 { FolderQuery } from '../../../../src/card-controller/folders/types';
import { FolderViewQuery } from '../../../../src/view/query'; import { FolderViewQuery } from '../../../../src/view/query';
import { createCardAPI, createFolder } from '../../../test-utils'; import { createCardAPI, createFolder } from '../../../test-utils';
@@ -7,7 +7,7 @@ import { createCardAPI, createFolder } from '../../../test-utils';
describe('should handle folder action', async () => { describe('should handle folder action', async () => {
it('should handle folder action successfully', async () => { it('should handle folder action successfully', async () => {
const api = createCardAPI(); const api = createCardAPI();
const action = new FolderAction( const action = new FoldersViewAction(
{}, {},
{ {
action: 'fire-dom-event', action: 'fire-dom-event',
@@ -20,7 +20,7 @@ describe('should handle folder action', async () => {
const query: FolderQuery = { const query: FolderQuery = {
folder, folder,
path: ['path'], path: [{ ha: { id: 'path' } }],
}; };
vi.mocked(api.getFoldersManager().generateDefaultFolderQuery).mockReturnValue(query); 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 () => { it('should do nothing with non-existent folder', async () => {
const api = createCardAPI(); const api = createCardAPI();
const action = new FolderAction( const action = new FoldersViewAction(
{}, {},
{ {
action: 'fire-dom-event', action: 'fire-dom-event',
@@ -64,7 +64,7 @@ describe('should handle folder action', async () => {
it('should do nothing with non-existent default query', async () => { it('should do nothing with non-existent default query', async () => {
const api = createCardAPI(); const api = createCardAPI();
const action = new FolderAction( const action = new FoldersViewAction(
{}, {},
{ {
action: 'fire-dom-event', 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 { DisplayModeSelectAction } from '../../../src/card-controller/actions/actions/display-mode-select';
import { DownloadAction } from '../../../src/card-controller/actions/actions/download'; import { DownloadAction } from '../../../src/card-controller/actions/actions/download';
import { ExpandAction } from '../../../src/card-controller/actions/actions/expand'; 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 { FullscreenAction } from '../../../src/card-controller/actions/actions/fullscreen';
import { InternalCallbackAction } from '../../../src/card-controller/actions/actions/internal-callback'; import { InternalCallbackAction } from '../../../src/card-controller/actions/actions/internal-callback';
import { LogAction } from '../../../src/card-controller/actions/actions/log'; import { LogAction } from '../../../src/card-controller/actions/actions/log';
@@ -185,7 +185,8 @@ describe('ActionFactory', () => {
}, },
InternalCallbackAction, 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', 'advanced_camera_card_action: $advanced_camera_card_action',
(action: Partial<ActionConfig>, classObject: object) => { (action: Partial<ActionConfig>, classObject: object) => {
@@ -1792,7 +1792,8 @@ describe('MenuButtonController', () => {
style: {}, style: {},
type: 'custom:advanced-camera-card-menu-icon', type: 'custom:advanced-camera-card-menu-icon',
title: 'Folders', 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, foldersManager,
view: createView({ view: createView({
view: 'folder', 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', type: 'custom:advanced-camera-card-menu-icon',
title: 'Folders', 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', view: 'folder',
query: new FolderViewQuery({ query: new FolderViewQuery({
folder: selectedFolder, folder: selectedFolder,
path: [{ id: 'id' }], path: [{ ha: { id: 'id' } }],
}), }),
}); });
@@ -1858,18 +1860,28 @@ describe('MenuButtonController', () => {
items: [ items: [
{ {
enabled: true, enabled: true,
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-0',
},
icon: 'mdi:folder', icon: 'mdi:folder',
selected: false, selected: false,
style: {}, style: {},
tap_action: { tap_action: {
action: 'fire-dom-event', action: 'fire-dom-event',
advanced_camera_card_action: 'folder', advanced_camera_card_action: 'folders',
folder: 'folder-0', folder: 'folder-0',
}, },
title: 'folder-0', title: 'folder-0',
}, },
{ {
enabled: true, enabled: true,
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-selected',
},
icon: 'mdi:folder', icon: 'mdi:folder',
selected: true, selected: true,
style: { style: {
@@ -1877,7 +1889,7 @@ describe('MenuButtonController', () => {
}, },
tap_action: { tap_action: {
action: 'fire-dom-event', action: 'fire-dom-event',
advanced_camera_card_action: 'folder', advanced_camera_card_action: 'folders',
folder: 'folder-selected', folder: 'folder-selected',
}, },
title: 'folder-selected', title: 'folder-selected',
@@ -1909,23 +1921,33 @@ describe('MenuButtonController', () => {
{ {
enabled: true, enabled: true,
icon: 'mdi:folder', icon: 'mdi:folder',
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-0',
},
selected: false, selected: false,
style: {}, style: {},
tap_action: { tap_action: {
action: 'fire-dom-event', action: 'fire-dom-event',
advanced_camera_card_action: 'folder', advanced_camera_card_action: 'folders',
folder: 'folder-0', folder: 'folder-0',
}, },
title: 'folder-0', title: 'folder-0',
}, },
{ {
enabled: true, enabled: true,
hold_action: {
action: 'fire-dom-event',
advanced_camera_card_action: 'folder',
folder: 'folder-1',
},
icon: 'mdi:folder', icon: 'mdi:folder',
selected: false, selected: false,
style: {}, style: {},
tap_action: { tap_action: {
action: 'fire-dom-event', action: 'fire-dom-event',
advanced_camera_card_action: 'folder', advanced_camera_card_action: 'folders',
folder: 'folder-1', folder: 'folder-1',
}, },
title: 'folder-1', title: 'folder-1',
+14 -9
View File
@@ -5,7 +5,7 @@ import { ActionConfig } from '../../src/config/schema/actions/types.js';
import { import {
createCameraAction, createCameraAction,
createDisplayModeAction, createDisplayModeAction,
createFolderAction, createFoldersViewAction,
createGeneralAction, createGeneralAction,
createInternalCallbackAction, createInternalCallbackAction,
createLogAction, createLogAction,
@@ -64,14 +64,19 @@ describe('createCameraAction', () => {
}); });
describe('createFolderAction', () => { describe('createFolderAction', () => {
it('should create folder action', () => { it.each([['folder' as const], ['folders' as const]])(
expect(createFolderAction({ folderID: 'folderID', cardID: 'card_id' })).toEqual({ '%s',
action: 'fire-dom-event', (viewName: 'folder' | 'folders') => {
advanced_camera_card_action: 'folder', expect(
card_id: 'card_id', createFoldersViewAction(viewName, { folderID: 'folderID', cardID: 'card_id' }),
folder: 'folderID', ).toEqual({
}); action: 'fire-dom-event',
}); advanced_camera_card_action: viewName,
card_id: 'card_id',
folder: 'folderID',
});
},
);
}); });
describe('createMediaPlayerAction', () => { describe('createMediaPlayerAction', () => {
+27 -24
View File
@@ -11,31 +11,34 @@ import {
describe('getCameraIDsForViewName', () => { describe('getCameraIDsForViewName', () => {
describe('views that are always supported', () => { describe('views that are always supported', () => {
it.each([['diagnostics' as const], ['image' as const], ['media' as const]])( it.each([
'%s', ['diagnostics' as const],
(viewName: AdvancedCameraCardView) => { ['folder' as const],
const cameraManager = createCameraManager(); ['folders' as const],
vi.mocked(cameraManager.getStore).mockReturnValue( ['image' as const],
createStore([ ['media' as const],
{ ])('%s', (viewName: AdvancedCameraCardView) => {
cameraID: 'camera-1', const cameraManager = createCameraManager();
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }), vi.mocked(cameraManager.getStore).mockReturnValue(
}, createStore([
{ cameraID: 'camera-2' }, {
]), cameraID: 'camera-1',
); config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
},
{ cameraID: 'camera-2' },
]),
);
expect(getCameraIDsForViewName(cameraManager, viewName)).toEqual( expect(getCameraIDsForViewName(cameraManager, viewName)).toEqual(
new Set(['camera-1', 'camera-2']), new Set(['camera-1', 'camera-2']),
); );
expect(getCameraIDsForViewName(cameraManager, viewName, 'camera-1')).toEqual( expect(getCameraIDsForViewName(cameraManager, viewName, 'camera-1')).toEqual(
new Set(['camera-1', 'camera-2']), new Set(['camera-1', 'camera-2']),
); );
expect(getCameraIDsForViewName(cameraManager, viewName, 'camera-2')).toEqual( expect(getCameraIDsForViewName(cameraManager, viewName, 'camera-2')).toEqual(
new Set(['camera-1', 'camera-2']), new Set(['camera-1', 'camera-2']),
); );
}, });
);
}); });
describe('views that respect dependencies and need a capability', () => { describe('views that respect dependencies and need a capability', () => {
+66 -41
View File
@@ -153,56 +153,81 @@ describe('View Basics', () => {
expect(view.context).toEqual({}); expect(view.context).toEqual({});
}); });
it('should detect gallery views', () => { describe('should detect folder views', () => {
expect(createView({ view: 'clips' }).isMediaGalleryView()).toBeTruthy(); it('should detect folder views', () => {
expect(createView({ view: 'folder' }).isMediaGalleryView()).toBeTruthy(); expect(createView({ view: 'folder' }).isAnyFolderView()).toBeTruthy();
expect(createView({ view: 'snapshots' }).isMediaGalleryView()).toBeTruthy(); expect(createView({ view: 'folders' }).isAnyFolderView()).toBeTruthy();
expect(createView({ view: 'recordings' }).isMediaGalleryView()).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', () => { describe('should detect media views', () => {
expect(createView({ view: 'live' }).isMediaGalleryView()).toBeFalsy(); it('should detect any media views', () => {
expect(createView({ view: 'timeline' }).isMediaGalleryView()).toBeFalsy(); 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', () => { describe('should detect gallery views', () => {
expect(createView({ view: 'clip' }).isAnyMediaView()).toBeTruthy(); it('should detect gallery views', () => {
expect(createView({ view: 'snapshot' }).isAnyMediaView()).toBeTruthy(); expect(createView({ view: 'clips' }).isMediaGalleryView()).toBeTruthy();
expect(createView({ view: 'media' }).isAnyMediaView()).toBeTruthy(); expect(createView({ view: 'folders' }).isMediaGalleryView()).toBeTruthy();
expect(createView({ view: 'recording' }).isAnyMediaView()).toBeTruthy(); expect(createView({ view: 'snapshots' }).isMediaGalleryView()).toBeTruthy();
expect(createView({ view: 'live' }).isAnyMediaView()).toBeTruthy(); expect(createView({ view: 'recordings' }).isMediaGalleryView()).toBeTruthy();
expect(createView({ view: 'image' }).isAnyMediaView()).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', () => { describe('should detect viewer views', () => {
expect(createView({ view: 'timeline' }).isAnyMediaView()).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();
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', () => { describe('should get default media type', () => {
expect(createView({ view: 'clip' }).isViewerView()).toBeTruthy(); it('should get default media type', () => {
expect(createView({ view: 'snapshot' }).isViewerView()).toBeTruthy(); expect(createView({ view: 'clip' }).getDefaultMediaType()).toBe('clips');
expect(createView({ view: 'media' }).isViewerView()).toBeTruthy(); expect(createView({ view: 'clips' }).getDefaultMediaType()).toBe('clips');
expect(createView({ view: 'recording' }).isViewerView()).toBeTruthy(); 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', () => { it('should not get default media type', () => {
expect(createView({ view: 'live' }).isViewerView()).toBeFalsy(); expect(createView({ view: 'live' }).getDefaultMediaType()).toBeNull();
expect(createView({ view: 'live' }).isViewerView()).toBeFalsy(); expect(createView({ view: 'timeline' }).getDefaultMediaType()).toBeNull();
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 determine if display mode is grid', () => { it('should determine if display mode is grid', () => {