Add support for precise layout cropping options.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import add from 'date-fns/add';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import sum from 'lodash-es/sum';
|
||||
import PQueue from 'p-queue';
|
||||
import { CardCameraAPI } from '../card-controller/types.js';
|
||||
import { CameraConfig, CamerasConfig, PTZAction, PTZPhase } from '../config/types.js';
|
||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
|
||||
@@ -108,6 +109,7 @@ export class CameraManager {
|
||||
protected _api: CardCameraAPI;
|
||||
protected _engineFactory: CameraManagerEngineFactory;
|
||||
protected _store: CameraManagerStore;
|
||||
protected _initializationLimit = new PQueue({ concurrency: 1 });
|
||||
|
||||
constructor(
|
||||
api: CardCameraAPI,
|
||||
@@ -138,8 +140,6 @@ export class CameraManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.reset();
|
||||
|
||||
// For each camera merge the config (which has no defaults) into the camera
|
||||
// global config (which does have defaults). The merging must happen in this
|
||||
// order, to ensure that the defaults in the cameras global config do not
|
||||
@@ -148,8 +148,16 @@ export class CameraManager {
|
||||
recursivelyMergeObjectsNotArrays({}, cloneDeep(config?.cameras_global), camera),
|
||||
);
|
||||
|
||||
try {
|
||||
const resetAndInitialize = async () => {
|
||||
await this.reset();
|
||||
await this._initializeCameras(cameras);
|
||||
};
|
||||
|
||||
try {
|
||||
// This concurrency limit prevents multiple rapidly arriving configs from
|
||||
// generating reset-n-initialize race conditions (e.g. changing values
|
||||
// rapidly in the config editor).
|
||||
await this._initializationLimit.add(resetAndInitialize);
|
||||
} catch (e: unknown) {
|
||||
this._api.getMessageManager().setErrorIfHigherPriority(e);
|
||||
return false;
|
||||
|
||||
@@ -618,6 +618,14 @@ const mediaLayoutConfigSchema = z.object({
|
||||
y: z.number().min(0).max(100).optional(),
|
||||
})
|
||||
.optional(),
|
||||
view_box: z
|
||||
.object({
|
||||
bottom: z.number().min(0).max(100).optional().default(0),
|
||||
left: z.number().min(0).max(100).optional().default(0),
|
||||
right: z.number().min(0).max(100).optional().default(0),
|
||||
top: z.number().min(0).max(100).optional().default(0),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
export type MediaLayoutConfig = z.infer<typeof mediaLayoutConfigSchema>;
|
||||
|
||||
|
||||
@@ -61,6 +61,14 @@ export const CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_X =
|
||||
`${CONF_CAMERAS}.#.dimensions.layout.position.x` as const;
|
||||
export const CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_Y =
|
||||
`${CONF_CAMERAS}.#.dimensions.layout.position.y` as const;
|
||||
export const CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_TOP =
|
||||
`${CONF_CAMERAS}.#.dimensions.layout.view_box.top` as const;
|
||||
export const CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_BOTTOM =
|
||||
`${CONF_CAMERAS}.#.dimensions.layout.view_box.bottom` as const;
|
||||
export const CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_LEFT =
|
||||
`${CONF_CAMERAS}.#.dimensions.layout.view_box.left` as const;
|
||||
export const CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_RIGHT =
|
||||
`${CONF_CAMERAS}.#.dimensions.layout.view_box.right` as const;
|
||||
export const CONF_CAMERAS_ARRAY_TRIGGERS_MOTION =
|
||||
`${CONF_CAMERAS}.#.triggers.motion` as const;
|
||||
export const CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY =
|
||||
|
||||
+80
-13
@@ -46,6 +46,10 @@ import {
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_FIT,
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_X,
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_Y,
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_BOTTOM,
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_LEFT,
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_RIGHT,
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_TOP,
|
||||
CONF_CAMERAS_ARRAY_FRIGATE_CAMERA_NAME,
|
||||
CONF_CAMERAS_ARRAY_FRIGATE_CLIENT_ID,
|
||||
CONF_CAMERAS_ARRAY_FRIGATE_LABELS,
|
||||
@@ -1226,6 +1230,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
configPathFit: string,
|
||||
configPathPositionX: string,
|
||||
configPathPositionY: string,
|
||||
configPathPositionTop: string,
|
||||
configPathPositionBottom: string,
|
||||
configPathPositionLeft: string,
|
||||
configPathPositionRight: string,
|
||||
): TemplateResult | void {
|
||||
return this._putInSubmenu(
|
||||
domain,
|
||||
@@ -1236,16 +1244,50 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
${this._renderOptionSelector(configPathFit, this._layoutFits, {
|
||||
label: localize('config.cameras.dimensions.layout.fit'),
|
||||
})}
|
||||
${this._renderNumberInput(configPathPositionX, {
|
||||
min: 0,
|
||||
max: 100,
|
||||
label: localize('config.cameras.dimensions.layout.position.x'),
|
||||
})}
|
||||
${this._renderNumberInput(configPathPositionY, {
|
||||
min: 0,
|
||||
max: 100,
|
||||
label: localize('config.cameras.dimensions.layout.position.y'),
|
||||
})}
|
||||
${this._putInSubmenu(
|
||||
`${domain}.position`,
|
||||
true,
|
||||
'config.cameras.dimensions.layout.position.editor_label',
|
||||
{ name: 'mdi:aspect-ratio' },
|
||||
html` ${this._renderNumberInput(configPathPositionX, {
|
||||
min: 0,
|
||||
max: 100,
|
||||
label: localize('config.cameras.dimensions.layout.position.x'),
|
||||
})}
|
||||
${this._renderNumberInput(configPathPositionY, {
|
||||
min: 0,
|
||||
max: 100,
|
||||
label: localize('config.cameras.dimensions.layout.position.y'),
|
||||
})}`,
|
||||
)}
|
||||
${this._putInSubmenu(
|
||||
`${domain}.view_box`,
|
||||
true,
|
||||
'config.cameras.dimensions.layout.view_box.editor_label',
|
||||
{ name: 'mdi:crop' },
|
||||
html`
|
||||
${this._renderNumberInput(configPathPositionTop, {
|
||||
min: 0,
|
||||
max: 100,
|
||||
label: localize('config.cameras.dimensions.layout.view_box.top'),
|
||||
})}
|
||||
${this._renderNumberInput(configPathPositionBottom, {
|
||||
min: 0,
|
||||
max: 100,
|
||||
label: localize('config.cameras.dimensions.layout.view_box.bottom'),
|
||||
})}
|
||||
${this._renderNumberInput(configPathPositionLeft, {
|
||||
min: 0,
|
||||
max: 100,
|
||||
label: localize('config.cameras.dimensions.layout.view_box.left'),
|
||||
})}
|
||||
${this._renderNumberInput(configPathPositionRight, {
|
||||
min: 0,
|
||||
max: 100,
|
||||
label: localize('config.cameras.dimensions.layout.view_box.right'),
|
||||
})}
|
||||
`,
|
||||
)}
|
||||
`,
|
||||
);
|
||||
}
|
||||
@@ -1971,9 +2013,34 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
${this._renderMediaLayout(
|
||||
MENU_CAMERAS_DIMENSIONS_LAYOUT,
|
||||
'config.cameras.dimensions.layout.editor_label',
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_FIT,
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_X,
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_Y,
|
||||
getArrayConfigPath(
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_FIT,
|
||||
cameraIndex,
|
||||
),
|
||||
getArrayConfigPath(
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_X,
|
||||
cameraIndex,
|
||||
),
|
||||
getArrayConfigPath(
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_Y,
|
||||
cameraIndex,
|
||||
),
|
||||
getArrayConfigPath(
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_TOP,
|
||||
cameraIndex,
|
||||
),
|
||||
getArrayConfigPath(
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_BOTTOM,
|
||||
cameraIndex,
|
||||
),
|
||||
getArrayConfigPath(
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_LEFT,
|
||||
cameraIndex,
|
||||
),
|
||||
getArrayConfigPath(
|
||||
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_VIEW_BOX_RIGHT,
|
||||
cameraIndex,
|
||||
),
|
||||
)}
|
||||
`,
|
||||
)}
|
||||
|
||||
@@ -56,8 +56,16 @@
|
||||
"fill": "El suport s'estira per omplir la targeta"
|
||||
},
|
||||
"position": {
|
||||
"editor_label": "",
|
||||
"x": "Percentatge d'emplaçament horitzontal",
|
||||
"y": "Percentatge d'emplaçament vertical"
|
||||
},
|
||||
"view_box": {
|
||||
"bottom": "",
|
||||
"editor_label": "",
|
||||
"left": "",
|
||||
"right": "",
|
||||
"top": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -56,8 +56,16 @@
|
||||
"fill": "Media is stretched to fill the card"
|
||||
},
|
||||
"position": {
|
||||
"editor_label": "Layout Cover Position",
|
||||
"x": "Horizontal placement percentage",
|
||||
"y": "Vertical placement percentage"
|
||||
},
|
||||
"view_box": {
|
||||
"bottom": "Bottom inset percentage",
|
||||
"editor_label": "Layout View Box",
|
||||
"left": "Left inset percentage",
|
||||
"right": "Right inset percentage",
|
||||
"top": "Top inset percentage"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -616,4 +624,4 @@
|
||||
"timeline": {
|
||||
"select_date": "Choose date"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,8 +56,16 @@
|
||||
"fill": "Il supporto viene allungato per riempire la scheda"
|
||||
},
|
||||
"position": {
|
||||
"editor_label": "",
|
||||
"x": "Percentuale di posizionamento orizzontale",
|
||||
"y": "Percentuale di posizionamento verticale"
|
||||
},
|
||||
"view_box": {
|
||||
"bottom": "",
|
||||
"editor_label": "",
|
||||
"left": "",
|
||||
"right": "",
|
||||
"top": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -56,8 +56,16 @@
|
||||
"fill": "Il supporto viene allungato per riempire la scheda"
|
||||
},
|
||||
"position": {
|
||||
"editor_label": "",
|
||||
"x": "Percentuale di posizionamento orizzontale",
|
||||
"y": "Percentuale di posizionamento verticale"
|
||||
},
|
||||
"view_box": {
|
||||
"bottom": "",
|
||||
"editor_label": "",
|
||||
"left": "",
|
||||
"right": "",
|
||||
"top": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -56,8 +56,16 @@
|
||||
"fill": "A mídia é esticada para preencher o cartão"
|
||||
},
|
||||
"position": {
|
||||
"editor_label": "",
|
||||
"x": "Porcentagem do posicionamento horizontal",
|
||||
"y": "Porcentagem do posicionamento vertical"
|
||||
},
|
||||
"view_box": {
|
||||
"bottom": "",
|
||||
"editor_label": "",
|
||||
"left": "",
|
||||
"right": "",
|
||||
"top": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -56,8 +56,16 @@
|
||||
"fill": "Preencher"
|
||||
},
|
||||
"position": {
|
||||
"editor_label": "",
|
||||
"x": "Percentagem da localização horizontal",
|
||||
"y": "Percentagem da localização vertical"
|
||||
},
|
||||
"view_box": {
|
||||
"bottom": "",
|
||||
"editor_label": "",
|
||||
"left": "",
|
||||
"right": "",
|
||||
"top": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,4 +2,13 @@
|
||||
object-fit: var(--frigate-card-media-layout-fit, contain);
|
||||
object-position: var(--frigate-card-media-layout-position-x, 50%)
|
||||
var(--frigate-card-media-layout-position-y, 50%);
|
||||
|
||||
// This is supported on limited browsers:
|
||||
// See: https://caniuse.com/?search=object-view-box
|
||||
object-view-box: inset(
|
||||
var(--frigate-card-media-layout-view-box-top, 0%)
|
||||
var(--frigate-card-media-layout-view-box-right, 0%)
|
||||
var(--frigate-card-media-layout-view-box-bottom, 0%)
|
||||
var(--frigate-card-media-layout-view-box-left, 0%)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,4 +24,14 @@ export const updateElementStyleFromMediaLayoutConfig = (
|
||||
element.style.removeProperty(`--frigate-card-media-layout-position-${dimension}`);
|
||||
}
|
||||
}
|
||||
for (const dimension of ['top', 'bottom', 'left', 'right']) {
|
||||
if (mediaLayoutConfig?.view_box?.[dimension] !== undefined) {
|
||||
element.style.setProperty(
|
||||
`--frigate-card-media-layout-view-box-${dimension}`,
|
||||
`${mediaLayoutConfig.view_box[dimension]}%`,
|
||||
);
|
||||
} else {
|
||||
element.style.removeProperty(`--frigate-card-media-layout-view-box-${dimension}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user