@@ -1,8 +1,4 @@
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import get from 'lodash-es/get';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import set from 'lodash-es/set';
|
||||
import unset from 'lodash-es/unset';
|
||||
import { cloneDeep, get, isEqual, set, unset } from 'lodash-es';
|
||||
import {
|
||||
CONF_AUTOMATIONS,
|
||||
CONF_CAMERAS,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
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>;
|
||||
@@ -7,8 +7,7 @@ const PTZ_BASE_ACTIONS = [...PTZ_PAN_TILT_ACTIONS, ...PTZ_ZOOM_ACTIONS] as const
|
||||
export type PTZBaseAction = (typeof PTZ_BASE_ACTIONS)[number];
|
||||
|
||||
// PTZ actions as used by the PTZ control (includes a 'home' button).
|
||||
const PTZ_CONTROL_ACTIONS = [...PTZ_BASE_ACTIONS, 'home'] as const;
|
||||
export type PTZControlAction = (typeof PTZ_CONTROL_ACTIONS)[number];
|
||||
export type PTZControlAction = PTZBaseAction | 'home';
|
||||
|
||||
// PTZ actions as used by the camera manager (includes generic presets).
|
||||
export const PTZ_ACTIONS = [...PTZ_BASE_ACTIONS, 'preset'] as const;
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import { z } from 'zod';
|
||||
import { VIEWS_USER_SPECIFIED } from '../../common/const';
|
||||
import {
|
||||
AdvancedCameraCardUserSpecifiedView,
|
||||
VIEWS_USER_SPECIFIED,
|
||||
} from '../../common/const';
|
||||
import { advancedCameraCardCustomActionsBaseSchema } from './base';
|
||||
|
||||
type AdvancedCameraCardUserSpecifiedViewWithoutFolder = Exclude<
|
||||
AdvancedCameraCardUserSpecifiedView,
|
||||
'folder'
|
||||
>;
|
||||
|
||||
export const viewActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
|
||||
advanced_camera_card_action: z.enum(VIEWS_USER_SPECIFIED),
|
||||
advanced_camera_card_action: z.enum(
|
||||
// The folder view is handled by the `folder` action since it accepts an
|
||||
// optional folder ID.
|
||||
VIEWS_USER_SPECIFIED.filter((view) => view !== 'folder') as [
|
||||
AdvancedCameraCardUserSpecifiedViewWithoutFolder,
|
||||
...AdvancedCameraCardUserSpecifiedViewWithoutFolder[],
|
||||
],
|
||||
),
|
||||
});
|
||||
export type ViewActionConfig = z.infer<typeof viewActionConfigSchema>;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { statusBarItemBaseSchema } from '../common/status-bar';
|
||||
import { advancedCameraCardCustomActionsBaseSchema } from './custom/base';
|
||||
import { cameraSelectActionConfigSchema } from './custom/camera-select';
|
||||
import { viewDisplayModeActionConfigSchema } from './custom/display-mode';
|
||||
import { folderActionConfigSchema } from './custom/folder';
|
||||
import { generalActionConfigSchema } from './custom/general';
|
||||
import { internalCallbackActionConfigSchema } from './custom/internal';
|
||||
import { logActionConfigSchema } from './custom/log';
|
||||
@@ -41,6 +42,7 @@ export const statusBarActionConfigSchema: z.ZodSchema<
|
||||
|
||||
const advancedCameraCardCustomActionSchema = z.union([
|
||||
cameraSelectActionConfigSchema,
|
||||
folderActionConfigSchema,
|
||||
generalActionConfigSchema,
|
||||
internalCallbackActionConfigSchema,
|
||||
logActionConfigSchema,
|
||||
|
||||
@@ -16,6 +16,7 @@ export const VIEWS_USER_SPECIFIED = [
|
||||
'live',
|
||||
'clip',
|
||||
'clips',
|
||||
'folder',
|
||||
'snapshot',
|
||||
'snapshots',
|
||||
'recording',
|
||||
@@ -24,12 +25,7 @@ export const VIEWS_USER_SPECIFIED = [
|
||||
'timeline',
|
||||
] as const;
|
||||
export type AdvancedCameraCardUserSpecifiedView = (typeof VIEWS_USER_SPECIFIED)[number];
|
||||
|
||||
const VIEWS = [
|
||||
...VIEWS_USER_SPECIFIED,
|
||||
'diagnostics',
|
||||
|
||||
// Media: A generic piece of media (could be clip, snapshot, recording).
|
||||
'media',
|
||||
] as const;
|
||||
export type AdvancedCameraCardView = (typeof VIEWS)[number];
|
||||
export type AdvancedCameraCardView =
|
||||
| AdvancedCameraCardUserSpecifiedView
|
||||
| 'media'
|
||||
| 'diagnostics';
|
||||
|
||||
@@ -2,10 +2,11 @@ import { z } from 'zod';
|
||||
|
||||
// The min/max width thumbnail.
|
||||
export const THUMBNAIL_WIDTH_MIN = 75;
|
||||
export const THUMBNAIL_WIDTH_DEFAULT = 100;
|
||||
export const THUMBNAIL_WIDTH_MAX = 300;
|
||||
|
||||
const thumbnailControlsBaseDefaults = {
|
||||
size: 100,
|
||||
size: THUMBNAIL_WIDTH_DEFAULT,
|
||||
show_details: true,
|
||||
show_favorite_control: true,
|
||||
show_timeline_control: true,
|
||||
@@ -13,7 +14,7 @@ const thumbnailControlsBaseDefaults = {
|
||||
};
|
||||
|
||||
// Configuration for the actual rendered thumbnail.
|
||||
const thumbnailsControlBaseSchema = z.object({
|
||||
export const thumbnailsControlBaseSchema = z.object({
|
||||
size: z
|
||||
.number()
|
||||
.min(THUMBNAIL_WIDTH_MIN)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const regexSchema = z.string().refine(
|
||||
(val) => {
|
||||
try {
|
||||
new RegExp(val);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ message: 'Invalid regular expression' },
|
||||
);
|
||||
@@ -1,21 +1,9 @@
|
||||
import { z } from 'zod';
|
||||
import { regexSchema } from '../../common/regex';
|
||||
|
||||
export const userAgentConditionSchema = z.object({
|
||||
condition: z.literal('user_agent'),
|
||||
user_agent: z.string().optional(),
|
||||
user_agent_re: z
|
||||
.string()
|
||||
.refine(
|
||||
(val) => {
|
||||
try {
|
||||
new RegExp(val);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ message: 'Invalid regular expression' },
|
||||
)
|
||||
.optional(),
|
||||
user_agent_re: regexSchema.optional(),
|
||||
companion: z.boolean().optional(),
|
||||
});
|
||||
|
||||
@@ -6,10 +6,10 @@ import { menuBaseSchema } from './base';
|
||||
export const menuSubmenuItemSchema = elementsBaseSchema.extend({
|
||||
entity: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
state_color: z.boolean().default(true),
|
||||
selected: z.boolean().default(false),
|
||||
state_color: z.boolean().default(true).optional(),
|
||||
selected: z.boolean().default(false).optional(),
|
||||
subtitle: z.string().optional(),
|
||||
enabled: z.boolean().default(true),
|
||||
enabled: z.boolean().default(true).optional(),
|
||||
});
|
||||
export type MenuSubmenuItem = z.infer<typeof menuSubmenuItemSchema>;
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { NonEmptyTuple } from 'type-fest';
|
||||
import { z } from 'zod';
|
||||
import { AdvancedCameraCardError } from '../../types';
|
||||
import { isTruthy } from '../../utils/basic';
|
||||
import { regexSchema } from './common/regex';
|
||||
|
||||
export const HA_MEDIA_SOURCE_ROOT = 'media-source://';
|
||||
|
||||
export const folderTypeSchema = z.enum(['ha']);
|
||||
export type FolderType = z.infer<typeof folderTypeSchema>;
|
||||
|
||||
const folderConfigDefault = {
|
||||
type: 'ha' as const,
|
||||
ha: {},
|
||||
};
|
||||
|
||||
const haFolderPathComponentSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
|
||||
title: z.string().optional(),
|
||||
title_re: regexSchema.optional(),
|
||||
});
|
||||
export type HAFolderPathComponent = z.infer<typeof haFolderPathComponentSchema>;
|
||||
|
||||
export const transformPathURLToPathArray = (
|
||||
url: string,
|
||||
): NonEmptyTuple<HAFolderPathComponent> => {
|
||||
let urlPath = url;
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
urlPath = urlObj.pathname;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {}
|
||||
|
||||
const splitPath = decodeURIComponent(urlPath).split(',').filter(isTruthy).slice(1);
|
||||
|
||||
// HA uses a pretty odd URL protocol for media-browser URLs:
|
||||
// - The URL is an encoded comma-separated value representing the folder
|
||||
// hierarchy
|
||||
// - The first component will be `media-browser/browser` representing the
|
||||
// root
|
||||
// - Each subsequent component will start with `media-source://<path>`
|
||||
// - All components except the last will additionally include
|
||||
// '/<media-class>'.
|
||||
const folderPath: NonEmptyTuple<HAFolderPathComponent> = [
|
||||
{ id: HA_MEDIA_SOURCE_ROOT },
|
||||
...splitPath.slice(0, -1).map((split) => ({ id: split.replace(/\/[^/]+$/, '') })),
|
||||
...splitPath.slice(-1).map((split) => ({ id: split })),
|
||||
];
|
||||
|
||||
for (const component of folderPath) {
|
||||
if (component.id && !component.id.startsWith(HA_MEDIA_SOURCE_ROOT)) {
|
||||
throw new AdvancedCameraCardError(
|
||||
`Could not parse valid media source URL: ${url}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return folderPath;
|
||||
};
|
||||
|
||||
const haFolderConfigSchema = z.object({
|
||||
url: z.string().transform(transformPathURLToPathArray).optional(),
|
||||
path: haFolderPathComponentSchema.array().nonempty().optional(),
|
||||
});
|
||||
export type HAFolderConfig = z.infer<typeof haFolderConfigSchema>;
|
||||
|
||||
const folderConfigSchema = z.object({
|
||||
type: folderTypeSchema.default(folderConfigDefault.type),
|
||||
id: z.string().optional(),
|
||||
ha: haFolderConfigSchema.default(folderConfigDefault.ha).optional(),
|
||||
title: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
});
|
||||
export type FolderConfig = z.infer<typeof folderConfigSchema>;
|
||||
|
||||
export const foldersConfigSchema = folderConfigSchema.array();
|
||||
@@ -1,45 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
import { actionsSchema } from './actions/types';
|
||||
import {
|
||||
thumbnailControlsDefaults,
|
||||
thumbnailsControlSchema,
|
||||
} from './common/controls/thumbnails';
|
||||
|
||||
const galleryThumbnailControlsDefaults = {
|
||||
...thumbnailControlsDefaults,
|
||||
show_details: false,
|
||||
};
|
||||
|
||||
export const galleryConfigDefault = {
|
||||
controls: {
|
||||
thumbnails: galleryThumbnailControlsDefaults,
|
||||
filter: {
|
||||
mode: 'right' as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const gallerythumbnailsControlSchema = thumbnailsControlSchema.extend({
|
||||
show_details: z.boolean().default(galleryThumbnailControlsDefaults.show_details),
|
||||
});
|
||||
|
||||
export const galleryConfigSchema = z
|
||||
.object({
|
||||
controls: z
|
||||
.object({
|
||||
thumbnails: gallerythumbnailsControlSchema.default(
|
||||
galleryConfigDefault.controls.thumbnails,
|
||||
),
|
||||
filter: z
|
||||
.object({
|
||||
mode: z
|
||||
.enum(['none', 'left', 'right'])
|
||||
.default(galleryConfigDefault.controls.filter.mode),
|
||||
})
|
||||
.default(galleryConfigDefault.controls.filter),
|
||||
})
|
||||
.default(galleryConfigDefault.controls),
|
||||
})
|
||||
.merge(actionsSchema)
|
||||
.default(galleryConfigDefault);
|
||||
export type GalleryConfig = z.infer<typeof galleryConfigSchema>;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { z } from 'zod';
|
||||
import { actionsSchema } from './actions/types';
|
||||
import {
|
||||
thumbnailControlsDefaults,
|
||||
thumbnailsControlBaseSchema,
|
||||
} from './common/controls/thumbnails';
|
||||
|
||||
const mediaGalleryThumbnailControlsDefaults = {
|
||||
...thumbnailControlsDefaults,
|
||||
show_details: false,
|
||||
};
|
||||
|
||||
export const mediaGalleryConfigDefault = {
|
||||
controls: {
|
||||
thumbnails: mediaGalleryThumbnailControlsDefaults,
|
||||
filter: {
|
||||
mode: 'right' as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mediaGallerythumbnailsControlSchema = thumbnailsControlBaseSchema.extend({
|
||||
show_details: z.boolean().default(mediaGalleryThumbnailControlsDefaults.show_details),
|
||||
});
|
||||
export type MediaGalleryThumbnailsConfig = z.infer<
|
||||
typeof mediaGallerythumbnailsControlSchema
|
||||
>;
|
||||
|
||||
export const mediaGalleryConfigSchema = z
|
||||
.object({
|
||||
controls: z
|
||||
.object({
|
||||
thumbnails: mediaGallerythumbnailsControlSchema.default(
|
||||
mediaGalleryConfigDefault.controls.thumbnails,
|
||||
),
|
||||
filter: z
|
||||
.object({
|
||||
mode: z
|
||||
.enum(['none', 'left', 'right'])
|
||||
.default(mediaGalleryConfigDefault.controls.filter.mode),
|
||||
})
|
||||
.default(mediaGalleryConfigDefault.controls.filter),
|
||||
})
|
||||
.default(mediaGalleryConfigDefault.controls),
|
||||
})
|
||||
.merge(actionsSchema)
|
||||
.default(mediaGalleryConfigDefault);
|
||||
export type MediaGalleryConfig = z.infer<typeof mediaGalleryConfigSchema>;
|
||||
@@ -34,6 +34,7 @@ export const menuConfigDefault = {
|
||||
display_mode: visibleButtonDefault,
|
||||
download: visibleButtonDefault,
|
||||
expand: hiddenButtonDefault,
|
||||
folders: visibleButtonDefault,
|
||||
iris: visibleButtonDefault,
|
||||
fullscreen: visibleButtonDefault,
|
||||
image: hiddenButtonDefault,
|
||||
@@ -82,6 +83,7 @@ export const menuConfigSchema = z
|
||||
),
|
||||
download: visibleButtonSchema.default(menuConfigDefault.buttons.download),
|
||||
expand: hiddenButtonSchema.default(menuConfigDefault.buttons.expand),
|
||||
folders: visibleButtonSchema.default(menuConfigDefault.buttons.folders),
|
||||
iris: visibleButtonSchema.default(menuConfigDefault.buttons.iris),
|
||||
fullscreen: visibleButtonSchema.default(menuConfigDefault.buttons.fullscreen),
|
||||
image: hiddenButtonSchema.default(menuConfigDefault.buttons.image),
|
||||
|
||||
@@ -8,9 +8,10 @@ import { imageConfigDefault } from './common/image';
|
||||
import { DebugConfig, debugConfigDefault, debugConfigSchema } from './debug';
|
||||
import { dimensionsConfigSchema } from './dimensions';
|
||||
import { pictureElementsSchema } from './elements/types';
|
||||
import { galleryConfigDefault, galleryConfigSchema } from './gallery';
|
||||
import { foldersConfigSchema } from './folders';
|
||||
import { imageConfigSchema } from './image';
|
||||
import { liveConfigDefault, liveConfigSchema } from './live';
|
||||
import { mediaGalleryConfigDefault, mediaGalleryConfigSchema } from './media-gallery';
|
||||
import { menuConfigDefault, menuConfigSchema } from './menu';
|
||||
import { overridesSchema } from './overrides';
|
||||
import {
|
||||
@@ -43,7 +44,7 @@ export const advancedCameraCardConfigSchema = z.object({
|
||||
menu: menuConfigSchema,
|
||||
status_bar: statusBarConfigSchema,
|
||||
live: liveConfigSchema,
|
||||
media_gallery: galleryConfigSchema,
|
||||
media_gallery: mediaGalleryConfigSchema,
|
||||
media_viewer: viewerConfigSchema,
|
||||
image: imageConfigSchema,
|
||||
elements: pictureElementsSchema,
|
||||
@@ -55,6 +56,8 @@ export const advancedCameraCardConfigSchema = z.object({
|
||||
|
||||
profiles: profilesSchema,
|
||||
|
||||
folders: foldersConfigSchema.optional(),
|
||||
|
||||
// Configuration overrides.
|
||||
overrides: overridesSchema,
|
||||
|
||||
@@ -78,7 +81,7 @@ export const configDefaults = {
|
||||
menu: menuConfigDefault,
|
||||
status_bar: statusBarConfigDefault,
|
||||
live: liveConfigDefault,
|
||||
media_gallery: galleryConfigDefault,
|
||||
media_gallery: mediaGalleryConfigDefault,
|
||||
media_viewer: viewerConfigDefault,
|
||||
image: imageConfigDefault,
|
||||
timeline: timelineConfigDefault,
|
||||
|
||||
@@ -36,16 +36,14 @@ const keyboardShortcutsSchema = z.object({
|
||||
});
|
||||
export type KeyboardShortcuts = z.infer<typeof keyboardShortcutsSchema>;
|
||||
|
||||
const KEYBOARD_SHORTCUT_PTZ_NAMES = [
|
||||
'ptz_down',
|
||||
'ptz_home',
|
||||
'ptz_left',
|
||||
'ptz_right',
|
||||
'ptz_up',
|
||||
'ptz_zoom_in',
|
||||
'ptz_zoom_out',
|
||||
] as const;
|
||||
export type PTZKeyboardShortcutName = (typeof KEYBOARD_SHORTCUT_PTZ_NAMES)[number];
|
||||
export type PTZKeyboardShortcutName =
|
||||
| 'ptz_down'
|
||||
| 'ptz_home'
|
||||
| 'ptz_left'
|
||||
| 'ptz_right'
|
||||
| 'ptz_up'
|
||||
| 'ptz_zoom_in'
|
||||
| 'ptz_zoom_out';
|
||||
|
||||
export const viewConfigDefault = {
|
||||
default: VIEW_DEFAULT,
|
||||
|
||||
Reference in New Issue
Block a user