Update dimensions and media layout options to be per camera.

This commit is contained in:
Dermot Duffy
2024-03-06 23:08:32 -08:00
parent b8e2b3218e
commit 72eae2bf88
20 changed files with 375 additions and 294 deletions
+7 -7
View File
@@ -1,7 +1,8 @@
import { StyleInfo } from 'lit/directives/style-map';
import { FrigateCardConfig } from '../config/types';
import { setPerformanceCSSStyles } from '../performance';
import { aspectRatioToStyle, setOrRemoveAttribute } from '../utils/basic';
import { View } from '../view/view';
import { setOrRemoveAttribute } from '../utils/basic';
import { CardStyleAPI } from './types';
export class StyleManager {
@@ -103,24 +104,23 @@ export class StyleManager {
* required).
* @returns A padding percentage.
*/
public getAspectRatioStyle(): string {
public getAspectRatioStyle(): StyleInfo {
const config = this._api.getConfigManager().getConfig();
const view = this._api.getViewManager().getView();
if (config) {
if (!this._isAspectRatioEnforced(config, view)) {
return 'auto';
return aspectRatioToStyle();
}
const aspectRatioMode = config.dimensions.aspect_ratio_mode;
const lastKnown = this._api.getMediaLoadedInfoManager().getLastKnown();
if (lastKnown && aspectRatioMode === 'dynamic') {
return `${lastKnown.width} / ${lastKnown.height}`;
return aspectRatioToStyle({ ratio: [lastKnown.width, lastKnown.height] });
}
return `${config.dimensions.aspect_ratio[0]} / ${config.dimensions.aspect_ratio[1]}`;
return aspectRatioToStyle({ ratio: config.dimensions.aspect_ratio });
}
return '16 / 9';
return aspectRatioToStyle({ defaultStatic: true });
}
}
+1 -4
View File
@@ -242,9 +242,6 @@ class FrigateCard extends LitElement {
return;
}
const cardStyle = {
'aspect-ratio': this._controller.getStyleManager().getAspectRatioStyle(),
};
const mainClasses = {
main: true,
'curve-top':
@@ -266,7 +263,7 @@ class FrigateCard extends LitElement {
hasHold: frigateCardHasAction(actions.hold_action),
hasDoubleClick: frigateCardHasAction(actions.double_tap_action),
})}
style="${styleMap(cardStyle)}"
style="${styleMap(this._controller.getStyleManager().getAspectRatioStyle())}"
@frigate-card:message=${(ev: CustomEvent<Message>) =>
this._controller.getMessageManager().setMessageIfHigherPriority(ev.detail)}
@frigate-card:view:change=${(ev: CustomEvent<View>) =>
+10 -2
View File
@@ -41,7 +41,7 @@ import {
Message,
} from '../../types.js';
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
import { contentsChanged } from '../../utils/basic.js';
import { aspectRatioToString, contentsChanged } from '../../utils/basic.js';
import { CarouselSelected } from '../../utils/embla/carousel-controller.js';
import { AutoLazyLoad } from '../../utils/embla/plugins/auto-lazy-load/auto-lazy-load.js';
import { AutoMediaActions } from '../../utils/embla/plugins/auto-media-actions/auto-media-actions.js';
@@ -900,7 +900,6 @@ export class FrigateCardLiveProvider
}
}
if (changedProps.has('liveConfig')) {
updateElementStyleFromMediaLayoutConfig(this, this.liveConfig?.layout);
if (this.liveConfig?.show_image_during_load) {
this._importPromises.push(import('./live-image.js'));
}
@@ -908,6 +907,7 @@ export class FrigateCardLiveProvider
this._importPromises.push(import('./../zoomer.js'));
}
}
if (changedProps.has('cameraConfig')) {
const provider = this._getResolvedProvider();
if (provider === 'jsmpeg') {
@@ -921,6 +921,14 @@ export class FrigateCardLiveProvider
} else if (provider === 'go2rtc') {
this._importPromises.push(import('./live-go2rtc.js'));
}
updateElementStyleFromMediaLayoutConfig(
this,
this.cameraConfig?.dimensions?.layout,
);
this.style.aspectRatio = aspectRatioToString({
ratio: this.cameraConfig?.dimensions?.aspect_ratio,
});
}
}
+13 -4
View File
@@ -37,6 +37,7 @@ import {
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
import { mayHaveAudio } from '../utils/audio.js';
import {
aspectRatioToString,
contentsChanged,
errorToConsole,
setOrRemoveAttribute,
@@ -396,10 +397,6 @@ export class FrigateCardViewerCarousel extends LitElement {
* @param changedProps The changed properties
*/
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('viewerConfig')) {
updateElementStyleFromMediaLayoutConfig(this, this.viewerConfig?.layout);
}
if (changedProps.has('view')) {
const newMedia =
this.view?.queryResults?.getResults(this.viewFilterCameraID) ?? null;
@@ -853,6 +850,18 @@ export class FrigateCardViewerProvider
if (changedProps.has('viewerConfig') && this.viewerConfig?.zoomable) {
import('./zoomer.js');
}
if (changedProps.has('media') || changedProps.has('cameraManager')) {
const cameraID = this.media?.getCameraID();
const cameraConfig = cameraID
? this.cameraManager?.getStore().getCameraConfig(cameraID)
: null;
updateElementStyleFromMediaLayoutConfig(this, cameraConfig?.dimensions?.layout);
this.style.aspectRatio = aspectRatioToString({
ratio: cameraConfig?.dimensions?.aspect_ratio,
});
}
}
protected _useZoomIfRequired(template: TemplateResult): TemplateResult {
+4
View File
@@ -6,6 +6,7 @@ import unset from 'lodash-es/unset';
import { RawFrigateCardConfig, RawFrigateCardConfigArray } from './config/types';
import {
CONF_CAMERAS,
CONF_CAMERAS_GLOBAL_DIMENSIONS_LAYOUT,
CONF_CAMERAS_GLOBAL_IMAGE,
CONF_CAMERAS_GLOBAL_JSMPEG,
CONF_CAMERAS_GLOBAL_WEBRTC_CARD,
@@ -565,4 +566,7 @@ const UPGRADES = [
transform: (val) => (val ? 'default' : null),
},
),
upgradeMoveToWithOverrides('live.layout', CONF_CAMERAS_GLOBAL_DIMENSIONS_LAYOUT),
deleteWithOverrides('media_viewer.layout'),
deleteWithOverrides('image.layout'),
];
+31 -21
View File
@@ -524,19 +524,19 @@ export type FrigateConditional = z.infer<typeof frigateConditionalSchema>;
// Cannot use discriminatedUnion since customSchema uses a superRefine, which
// causes false rejections.
const pictureElementSchema = z.union([
menuStateIconSchema,
conditionalSchema,
customSchema,
frigateConditionalSchema,
iconSchema,
imageSchema,
menuIconSchema,
menuStateIconSchema,
menuSubmenuSchema,
menuSubmenuSelectSchema,
frigateConditionalSchema,
serviceCallButtonSchema,
stateBadgeIconSchema,
stateIconSchema,
stateLabelSchema,
serviceCallButtonSchema,
iconSchema,
imageSchema,
conditionalSchema,
customSchema,
]);
const pictureElementsSchema = pictureElementSchema.array().optional();
export type PictureElements = z.infer<typeof pictureElementsSchema>;
@@ -556,6 +556,21 @@ const mediaLayoutConfigSchema = z.object({
});
export type MediaLayoutConfig = z.infer<typeof mediaLayoutConfigSchema>;
// *************************************************************************
// Aspect Ratio Configuration
// *************************************************************************
const aspectRatioSchema = z
.number()
.array()
.length(2)
.or(
z
.string()
.regex(/^\s*\d+\.?\d*\s*[:/]\s*\d+\.?\d*\s*$/)
.transform((input) => input.split(/[:\/]/).map((d) => Number(d))),
);
// *************************************************************************
// Image Configuration
// Image config base options are used both for the `image` live provider and the
@@ -581,7 +596,6 @@ const imageConfigDefault = {
const imageConfigSchema = imageBaseConfigSchema
.extend({
mode: z.enum(IMAGE_MODES).default(imageConfigDefault.mode),
layout: mediaLayoutConfigSchema.optional(),
zoomable: z.boolean().default(imageConfigDefault.zoomable),
})
.merge(actionsSchema)
@@ -711,6 +725,7 @@ export type TimelineConfig = z.infer<typeof timelineConfigSchema>;
// *************************************************************************
// Next/Previous Control Configuration
// *************************************************************************
const nextPreviousControlConfigSchema = z.object({
style: z.enum(['none', 'chevrons', 'icons', 'thumbnails']),
size: z.number().min(BUTTON_SIZE_MIN),
@@ -950,7 +965,6 @@ const liveOverridableConfigSchema = z
show_image_during_load: z
.boolean()
.default(liveConfigDefault.show_image_during_load),
layout: mediaLayoutConfigSchema.optional(),
microphone: microphoneConfigSchema.default(liveConfigDefault.microphone),
zoomable: z.boolean().default(liveConfigDefault.zoomable),
display: viewDisplaySchema,
@@ -1138,6 +1152,13 @@ export const cameraConfigSchema = z
webrtc_card: webrtcCardConfigSchema.optional(),
cast: castSchema.optional(),
dimensions: z
.object({
aspect_ratio: aspectRatioSchema.optional(),
layout: mediaLayoutConfigSchema.optional(),
})
.optional(),
})
.default(cameraConfigDefault);
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
@@ -1412,7 +1433,6 @@ const viewerConfigSchema = z
title: titleControlConfigSchema.optional(),
})
.default(viewerConfigDefault.controls),
layout: mediaLayoutConfigSchema.optional(),
})
.merge(actionsSchema)
.default(viewerConfigDefault);
@@ -1477,17 +1497,7 @@ export const dimensionsConfigSchema = z
aspect_ratio_mode: z
.enum(['dynamic', 'static', 'unconstrained'])
.default(dimensionsConfigDefault.aspect_ratio_mode),
aspect_ratio: z
.number()
.array()
.length(2)
.or(
z
.string()
.regex(/^\s*\d+\s*[:/]\s*\d+\s*$/)
.transform((input) => input.split(/[:\/]/).map((d) => Number(d))),
)
.default(dimensionsConfigDefault.aspect_ratio),
aspect_ratio: aspectRatioSchema.default(dimensionsConfigDefault.aspect_ratio),
max_height: z.string().default(dimensionsConfigDefault.max_height),
min_height: z.string().default(dimensionsConfigDefault.min_height),
})
+10 -11
View File
@@ -11,6 +11,8 @@ export const CONF_CAMERAS_ARRAY_CAST_DASHBOARD_DASHBOARD_PATH =
`${CONF_CAMERAS}.#.cast.dashboard.dashboard_path` as const;
export const CONF_CAMERAS_ARRAY_CAST_DASHBOARD_VIEW_PATH =
`${CONF_CAMERAS}.#.cast.dashboard.view_path` as const;
export const CONF_CAMERAS_ARRAY_DIMENSIONS_ASPECT_RATIO =
`${CONF_CAMERAS}.#.dimensions.aspect_ratio` as const;
export const CONF_CAMERAS_ARRAY_FRIGATE_CLIENT_ID =
`${CONF_CAMERAS}.#.frigate.client_id` as const;
export const CONF_CAMERAS_ARRAY_FRIGATE_LABELS =
@@ -48,6 +50,12 @@ export const CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS =
`${CONF_CAMERAS}.#.dependencies.cameras` as const;
export const CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS =
`${CONF_CAMERAS}.#.dependencies.all_cameras` as const;
export const CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_FIT =
`${CONF_CAMERAS}.#.dimensions.layout.fit` as const;
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_TRIGGERS_MOTION =
`${CONF_CAMERAS}.#.triggers.motion` as const;
export const CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY =
@@ -66,6 +74,8 @@ export const CONF_CAMERAS_GLOBAL_TRIGGERS_OCCUPANCY =
`${CONF_CAMERAS_GLOBAL}.triggers.occupancy` as const;
export const CONF_CAMERAS_GLOBAL_IMAGE_REFRESH_SECONDS =
`${CONF_CAMERAS_GLOBAL}.image.refresh_seconds` as const;
export const CONF_CAMERAS_GLOBAL_DIMENSIONS_LAYOUT =
`${CONF_CAMERAS_GLOBAL}.dimensions.layout` as const;
export const CONF_ELEMENTS = 'elements' as const;
@@ -163,11 +173,6 @@ export const CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE =
`${CONF_MEDIA_VIEWER}.controls.title.mode` as const;
export const CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS =
`${CONF_MEDIA_VIEWER}.controls.title.duration_seconds` as const;
export const CONF_MEDIA_VIEWER_LAYOUT_FIT = `${CONF_MEDIA_VIEWER}.layout.fit` as const;
export const CONF_MEDIA_VIEWER_LAYOUT_POSITION_X =
`${CONF_MEDIA_VIEWER}.layout.position.x` as const;
export const CONF_MEDIA_VIEWER_LAYOUT_POSITION_Y =
`${CONF_MEDIA_VIEWER}.layout.position.y` as const;
const CONF_LIVE = 'live' as const;
export const CONF_LIVE_AUTO_PLAY = `${CONF_LIVE}.auto_play` as const;
@@ -228,9 +233,6 @@ export const CONF_LIVE_DISPLAY_GRID_MAX_COLUMNS =
`${CONF_LIVE}.display.grid_max_columns` as const;
export const CONF_LIVE_DISPLAY_GRID_SELECTED_WIDTH_FACTOR =
`${CONF_LIVE}.display.grid_selected_width_factor` as const;
export const CONF_LIVE_LAYOUT_FIT = `${CONF_LIVE}.layout.fit` as const;
export const CONF_LIVE_LAYOUT_POSITION_X = `${CONF_LIVE}.layout.position.x` as const;
export const CONF_LIVE_LAYOUT_POSITION_Y = `${CONF_LIVE}.layout.position.y` as const;
export const CONF_LIVE_DRAGGABLE = `${CONF_LIVE}.draggable` as const;
export const CONF_LIVE_LAZY_LOAD = `${CONF_LIVE}.lazy_load` as const;
export const CONF_LIVE_LAZY_UNLOAD = `${CONF_LIVE}.lazy_unload` as const;
@@ -247,9 +249,6 @@ export const CONF_LIVE_MICROPHONE_ALWAYS_CONNECTED =
export const CONF_LIVE_ZOOMABLE = `${CONF_LIVE}.zoomable` as const;
const CONF_IMAGE = 'image' as const;
export const CONF_IMAGE_LAYOUT_FIT = `${CONF_IMAGE}.layout.fit` as const;
export const CONF_IMAGE_LAYOUT_POSITION_X = `${CONF_IMAGE}.layout.position.x` as const;
export const CONF_IMAGE_LAYOUT_POSITION_Y = `${CONF_IMAGE}.layout.position.y` as const;
export const CONF_IMAGE_MODE = `${CONF_IMAGE}.mode` as const;
export const CONF_IMAGE_REFRESH_SECONDS = `${CONF_IMAGE}.refresh_seconds` as const;
export const CONF_IMAGE_URL = `${CONF_IMAGE}.url` as const;
+39 -45
View File
@@ -22,9 +22,9 @@ import {
} from './config-mgmt.js';
import {
BUTTON_SIZE_MIN,
FRIGATE_MENU_PRIORITY_MAX,
FrigateCardConfig,
frigateCardConfigDefaults,
FRIGATE_MENU_PRIORITY_MAX,
RawFrigateCardConfig,
RawFrigateCardConfigArray,
THUMBNAIL_WIDTH_MAX,
@@ -38,6 +38,10 @@ import {
CONF_CAMERAS_ARRAY_CAST_METHOD,
CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS,
CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS,
CONF_CAMERAS_ARRAY_DIMENSIONS_ASPECT_RATIO,
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_FIT,
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_X,
CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_POSITION_Y,
CONF_CAMERAS_ARRAY_FRIGATE_CAMERA_NAME,
CONF_CAMERAS_ARRAY_FRIGATE_CLIENT_ID,
CONF_CAMERAS_ARRAY_FRIGATE_LABELS,
@@ -67,9 +71,6 @@ import {
CONF_DIMENSIONS_ASPECT_RATIO_MODE,
CONF_DIMENSIONS_MAX_HEIGHT,
CONF_DIMENSIONS_MIN_HEIGHT,
CONF_IMAGE_LAYOUT_FIT,
CONF_IMAGE_LAYOUT_POSITION_X,
CONF_IMAGE_LAYOUT_POSITION_Y,
CONF_IMAGE_MODE,
CONF_IMAGE_REFRESH_SECONDS,
CONF_IMAGE_URL,
@@ -108,9 +109,6 @@ import {
CONF_LIVE_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
CONF_LIVE_DISPLAY_MODE,
CONF_LIVE_DRAGGABLE,
CONF_LIVE_LAYOUT_FIT,
CONF_LIVE_LAYOUT_POSITION_X,
CONF_LIVE_LAYOUT_POSITION_Y,
CONF_LIVE_LAZY_LOAD,
CONF_LIVE_LAZY_UNLOAD,
CONF_LIVE_MICROPHONE_ALWAYS_CONNECTED,
@@ -152,16 +150,13 @@ import {
CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
CONF_MEDIA_VIEWER_DISPLAY_MODE,
CONF_MEDIA_VIEWER_DRAGGABLE,
CONF_MEDIA_VIEWER_LAYOUT_FIT,
CONF_MEDIA_VIEWER_LAYOUT_POSITION_X,
CONF_MEDIA_VIEWER_LAYOUT_POSITION_Y,
CONF_MEDIA_VIEWER_LAZY_LOAD,
CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP,
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
CONF_MEDIA_VIEWER_ZOOMABLE,
CONF_MENU_ALIGNMENT,
CONF_MENU_BUTTON_SIZE,
CONF_MENU_BUTTONS,
CONF_MENU_BUTTON_SIZE,
CONF_MENU_POSITION,
CONF_MENU_STYLE,
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
@@ -213,6 +208,8 @@ const MENU_BUTTONS = 'buttons';
const MENU_CAMERAS = 'cameras';
const MENU_CAMERAS_CAST = 'cameras.cast';
const MENU_CAMERAS_DEPENDENCIES = 'cameras.dependencies';
const MENU_CAMERAS_DIMENSIONS = 'cameras.dimensions';
const MENU_CAMERAS_DIMENSIONS_LAYOUT = 'cameras.dimensions.layout';
const MENU_CAMERAS_ENGINE = 'cameras.engine';
const MENU_CAMERAS_FRIGATE = 'cameras.frigate';
const MENU_CAMERAS_GO2RTC = 'cameras.go2rtc';
@@ -221,7 +218,6 @@ const MENU_CAMERAS_LIVE_PROVIDER = 'cameras.live_provider';
const MENU_CAMERAS_MOTIONEYE = 'cameras.motioneye';
const MENU_CAMERAS_TRIGGERS = 'cameras.triggers';
const MENU_CAMERAS_WEBRTC_CARD = 'cameras.webrtc_card';
const MENU_IMAGE_LAYOUT = 'image.layout';
const MENU_LIVE_CONTROLS = 'live.controls';
const MENU_LIVE_CONTROLS_NEXT_PREVIOUS = 'live.controls.next_previous';
const MENU_LIVE_CONTROLS_PTZ = 'live.controls.ptz';
@@ -229,7 +225,6 @@ const MENU_LIVE_CONTROLS_THUMBNAILS = 'live.controls.thumbnails';
const MENU_LIVE_CONTROLS_TIMELINE = 'live.controls.timeline';
const MENU_LIVE_CONTROLS_TITLE = 'live.controls.title';
const MENU_LIVE_DISPLAY = 'live.display';
const MENU_LIVE_LAYOUT = 'live.layout';
const MENU_LIVE_MICROPHONE = 'live.microphone';
const MENU_MEDIA_GALLERY_CONTROLS_FILTER = 'media_gallery.controls.filter';
const MENU_MEDIA_GALLERY_CONTROLS_THUMBNAILS = 'media_gallery.controls.thumbnails';
@@ -239,7 +234,6 @@ const MENU_MEDIA_VIEWER_CONTROLS_THUMBNAILS = 'media_viewer.controls.thumbnails'
const MENU_MEDIA_VIEWER_CONTROLS_TIMELINE = 'media_viewer.controls.timeline';
const MENU_MEDIA_VIEWER_CONTROLS_TITLE = 'media_viewer.controls.title';
const MENU_MEDIA_VIEWER_DISPLAY = 'media_viewer.display';
const MENU_MEDIA_VIEWER_LAYOUT = 'media_viewer.layout';
const MENU_OPTIONS = 'options';
const MENU_PERFORMANCE_FEATURES = 'performance.features';
const MENU_PERFORMANCE_STYLE = 'performance.style';
@@ -576,9 +570,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
protected _layoutFits: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'contain', label: localize('config.common.layout.fits.contain') },
{ value: 'cover', label: localize('config.common.layout.fits.cover') },
{ value: 'fill', label: localize('config.common.layout.fits.fill') },
{
value: 'contain',
label: localize('config.cameras.dimensions.layout.fits.contain'),
},
{ value: 'cover', label: localize('config.cameras.dimensions.layout.fits.cover') },
{ value: 'fill', label: localize('config.cameras.dimensions.layout.fits.fill') },
];
protected _miniTimelineModes: EditorSelectOption[] = [
@@ -1167,17 +1164,17 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
{ name: 'mdi:page-layout-body' },
html`
${this._renderOptionSelector(configPathFit, this._layoutFits, {
label: localize('config.common.layout.fit'),
label: localize('config.cameras.dimensions.layout.fit'),
})}
${this._renderNumberInput(configPathPositionX, {
min: 0,
max: 100,
label: localize('config.common.layout.position.x'),
label: localize('config.cameras.dimensions.layout.position.x'),
})}
${this._renderNumberInput(configPathPositionY, {
min: 0,
max: 100,
label: localize('config.common.layout.position.y'),
label: localize('config.cameras.dimensions.layout.position.y'),
})}
`,
);
@@ -1854,10 +1851,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
},
)}
${this._renderOptionSelector(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_TRIGGERS_EVENTS,
cameraIndex,
),
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_EVENTS, cameraIndex),
this._triggersEvents,
{
multiple: true,
@@ -1890,6 +1884,27 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
)}
`,
)}
${this._putInSubmenu(
MENU_CAMERAS_DIMENSIONS,
cameraIndex,
'config.cameras.dimensions.editor_label',
{ name: 'mdi:aspect-ratio' },
html`
${this._renderStringInput(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_DIMENSIONS_ASPECT_RATIO,
cameraIndex,
),
)}
${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,
)}
`,
)}
</div>`
: ``}
</div>
@@ -2237,13 +2252,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
)}
`,
)}
${this._renderMediaLayout(
MENU_LIVE_LAYOUT,
'config.live.layout',
CONF_LIVE_LAYOUT_FIT,
CONF_LIVE_LAYOUT_POSITION_X,
CONF_LIVE_LAYOUT_POSITION_Y,
)}
${this._putInSubmenu(
MENU_LIVE_MICROPHONE,
true,
@@ -2389,13 +2397,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
)}
`,
)}
${this._renderMediaLayout(
MENU_MEDIA_VIEWER_LAYOUT,
'config.media_viewer.layout',
CONF_MEDIA_VIEWER_LAYOUT_FIT,
CONF_MEDIA_VIEWER_LAYOUT_POSITION_X,
CONF_MEDIA_VIEWER_LAYOUT_POSITION_Y,
)}
</div>`
: ''}
${this._renderOptionSetHeader('image')}
@@ -2405,13 +2406,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderStringInput(CONF_IMAGE_URL)}
${this._renderNumberInput(CONF_IMAGE_REFRESH_SECONDS)}
${this._renderSwitch(CONF_IMAGE_ZOOMABLE, this._defaults.image.zoomable)}
${this._renderMediaLayout(
MENU_IMAGE_LAYOUT,
'config.image.layout',
CONF_IMAGE_LAYOUT_FIT,
CONF_IMAGE_LAYOUT_POSITION_X,
CONF_IMAGE_LAYOUT_POSITION_Y,
)}
</div>`
: ''}
${this._renderOptionSetHeader('timeline')}
+22 -20
View File
@@ -15,7 +15,7 @@
"dashboard_path": "Dashboard path",
"view_path": "View path"
},
"editor_label": "Cast Options",
"editor_label": "Cast options",
"method": "Cast method",
"methods": {
"dashboard": "Dashboard",
@@ -25,7 +25,24 @@
"dependencies": {
"all_cameras": "Show events for all cameras with this camera",
"cameras": "Show events for specific cameras with this camera",
"editor_label": "Dependency Options"
"editor_label": "Dependency options"
},
"dimensions": {
"aspect_ratio": "Static aspect ratio",
"editor_label": "Camera dimensions",
"layout": {
"editor_label": "Camera Layout",
"fit": "Layout fit",
"fits": {
"contain": "Media is contained/letterboxed",
"cover": "Media expands proportionally to cover the card",
"fill": "Media is stretched to fill the card"
},
"position": {
"x": "Horizontal placement percentage",
"y": "Vertical placement percentage"
}
}
},
"engines": {
"editor_label": "Camera engine options"
@@ -83,7 +100,7 @@
},
"title": "Title for this camera (Autodetected from entity)",
"triggers": {
"editor_label": "Trigger Options",
"editor_label": "Trigger options",
"entities": "Trigger from other entities",
"events": {
"clips": "Events with new clips",
@@ -178,18 +195,6 @@
"grid_selected_width_factor": "Increase selected media width by this factor",
"mode": "Mode"
},
"layout": {
"fit": "Layout fit",
"fits": {
"contain": "Media is contained/letterboxed",
"cover": "Media expands proportionally to cover the card",
"fill": "Media is stretched to fill the card"
},
"position": {
"x": "Horizontal placement percentage",
"y": "Vertical placement percentage"
}
},
"media_action_conditions": {
"all": "All opportunities",
"hidden": "On browser/tab hiding",
@@ -229,7 +234,6 @@
"min_height": "Minimum card height in CSS units (e.g. '100px')"
},
"image": {
"layout": "Image Layout",
"mode": "Image view mode",
"modes": {
"camera": "Home Assistant camera snapshot of camera entity",
@@ -272,7 +276,6 @@
}
},
"draggable": "Live cameras view can be dragged/swiped",
"layout": "Live Layout",
"lazy_load": "Live cameras are lazily loaded",
"lazy_unload": "Live cameras are lazily unloaded",
"microphone": {
@@ -296,7 +299,6 @@
"editor_label": "Media Viewer Controls"
},
"draggable": "Media Viewer can be dragged/swiped",
"layout": "Media Viewer Layout",
"lazy_load": "Media Viewer media is lazily loaded in carousel",
"snapshot_click_plays_clip": "Clicking on a snapshot plays a related clip",
"transition_effect": "Media Viewer transition effect",
@@ -454,8 +456,8 @@
"cameras": "Cameras",
"cameras_secondary": "What cameras to render on this card",
"delete": "Delete",
"dimensions": "Dimensions",
"dimensions_secondary": "Dimensions & shape options",
"dimensions": "Card dimensions",
"dimensions_secondary": "Card dimensions & shape options",
"image": "Image",
"image_secondary": "Static image view options",
"live": "Live",
+19 -18
View File
@@ -27,6 +27,22 @@
"cameras": "Mostra eventi per telecamere specifiche con questa telecamera",
"editor_label": "Opzioni di dipendenza"
},
"dimensions": {
"editor_label": "",
"aspect_ratio": "",
"layout": {
"fit": "Adatta al layout",
"fits": {
"contain": "Il supporto è contenuto/in cassetta delle lettere",
"cover": "Il supporto si espande proporzionalmente per coprire la scheda",
"fill": "Il supporto viene allungato per riempire la scheda"
},
"position": {
"x": "Percentuale di posizionamento orizzontale",
"y": "Percentuale di posizionamento verticale"
}
}
},
"engines": {
"editor_label": "Opzioni del motore della fotocamera"
},
@@ -178,18 +194,6 @@
"grid_selected_width_factor": "",
"mode": ""
},
"layout": {
"fit": "Adatta al layout",
"fits": {
"contain": "Il supporto è contenuto/in cassetta delle lettere",
"cover": "Il supporto si espande proporzionalmente per coprire la scheda",
"fill": "Il supporto viene allungato per riempire la scheda"
},
"position": {
"x": "Percentuale di posizionamento orizzontale",
"y": "Percentuale di posizionamento verticale"
}
},
"media_action_conditions": {
"all": "Tutte le opportunità",
"hidden": "Sul browser/nascondere le schede",
@@ -229,7 +233,6 @@
"min_height": ""
},
"image": {
"layout": "Disposizione dell'immagine",
"mode": "Modalità Visualizza immagine",
"modes": {
"camera": "Istantanea della telecamera di Home Assistant dell'entità telecamera",
@@ -272,7 +275,6 @@
}
},
"draggable": "Il Visualizzatore eventi può essere trascinato oppure puoi scorrere",
"layout": "Disposizione dal vivo",
"lazy_load": "Le telecamere dal vivo sono pigramente cariche",
"lazy_unload": "Le telecamere dal vivo sono pigramente non caricate",
"microphone": {
@@ -296,7 +298,6 @@
"editor_label": "Controlli di visualizzatore multimediale"
},
"draggable": "Il visualizzatore multimediale può essere trascinato oppure può scorrere",
"layout": "Layout del visualizzatore multimediale",
"lazy_load": "Il media Viewer viene caricato pigramente nel carosello",
"transition_effect": "Effetto di transizione del visualizzatore multimediale",
"transition_effects": {
@@ -448,8 +449,8 @@
"cameras": "Telecamere",
"cameras_secondary": "Quali telecamere visualizzare su questa card",
"delete": "Elimina",
"dimensions": "Dimensioni",
"dimensions_secondary": "Dimensioni e opzioni di forma",
"dimensions": "",
"dimensions_secondary": "",
"image": "Immagine",
"image_secondary": "Opzioni di visualizzazione dell'immagine statica",
"live": "Live",
@@ -584,4 +585,4 @@
},
"select_date": "Scegli la data"
}
}
}
+19 -18
View File
@@ -27,6 +27,22 @@
"cameras": "Mostrar eventos para câmeras específicas nesta câmera",
"editor_label": "Opções de dependência"
},
"dimensions": {
"editor_label": "",
"aspect_ratio": "",
"layout": {
"fit": "Ajuste de layout",
"fits": {
"contain": "A mídia é contida no cartão",
"cover": "A mídia se expande proporcionalmente para cobrir o cartão",
"fill": "A mídia é esticada para preencher o cartão"
},
"position": {
"x": "Porcentagem do posicionamento horizontal",
"y": "Porcentagem do posicionamento vertical"
}
}
},
"engines": {
"editor_label": "Opções do motor da câmera"
},
@@ -178,18 +194,6 @@
"grid_selected_width_factor": "",
"mode": ""
},
"layout": {
"fit": "Ajuste de layout",
"fits": {
"contain": "A mídia é contida no cartão",
"cover": "A mídia se expande proporcionalmente para cobrir o cartão",
"fill": "A mídia é esticada para preencher o cartão"
},
"position": {
"x": "Porcentagem do posicionamento horizontal",
"y": "Porcentagem do posicionamento vertical"
}
},
"media_action_conditions": {
"all": "Todas as oportunidades",
"hidden": "Ao ocultar o navegador/aba",
@@ -229,7 +233,6 @@
"min_height": ""
},
"image": {
"layout": "Layout da imagem",
"mode": "Modo de visualização de imagem",
"modes": {
"camera": "Instantâneo da câmera do Home Assistant, da entidade de câmera",
@@ -272,7 +275,6 @@
}
},
"draggable": "A visualização ao vivo das câmeras pode ser arrastada/deslizada",
"layout": "Layout dinâmico",
"lazy_load": "As câmeras ao vivo são carregadas lentamente",
"lazy_unload": "As câmeras ao vivo são descarregadas preguiçosamente",
"microphone": {
@@ -296,7 +298,6 @@
"editor_label": "Controles do visualizador de mídia"
},
"draggable": "Visualizador de eventos pode ser arrastado/deslizado",
"layout": "Layout do visualizador de mídia",
"lazy_load": "A mídia do Visualizador de eventos é carregada lentamente no carrossel",
"snapshot_click_plays_clip": "Clicar em um instantâneo reproduz um clipe relacionado",
"transition_effect": "Efeito de transição do Visualizador de eventos",
@@ -453,8 +454,8 @@
"cameras": "Câmeras",
"cameras_secondary": "Quais câmeras renderizar neste cartão",
"delete": "Excluir",
"dimensions": "Dimensões",
"dimensions_secondary": "Dimensões e opções de forma",
"dimensions": "",
"dimensions_secondary": "",
"image": "Imagem",
"image_secondary": "Opções de visualização de imagem estática",
"live": "Ao vivo",
@@ -594,4 +595,4 @@
},
"select_date": "Escolha a data"
}
}
}
+19 -18
View File
@@ -27,6 +27,22 @@
"cameras": "Mostrar eventos para câmeras específicas nesta câmera",
"editor_label": "Opções de dependência"
},
"dimensions": {
"editor_label": "",
"aspect_ratio": "",
"layout": {
"fit": "Fit",
"fits": {
"contain": "Conter",
"cover": "Tapar",
"fill": "Preencher"
},
"position": {
"x": "Percentagem da localização horizontal",
"y": "Percentagem da localização vertical"
}
}
},
"engines": {
"editor_label": "Editor de etiquetas"
},
@@ -178,18 +194,6 @@
"grid_selected_width_factor": "",
"mode": ""
},
"layout": {
"fit": "Fit",
"fits": {
"contain": "Conter",
"cover": "Tapar",
"fill": "Preencher"
},
"position": {
"x": "Percentagem da localização horizontal",
"y": "Percentagem da localização vertical"
}
},
"media_action_conditions": {
"all": "Todas as oportunidades",
"hidden": "Ao ocultar o navegador/aba",
@@ -222,7 +226,6 @@
}
},
"image": {
"layout": "Layout",
"mode": "Modo de visualização de imagem",
"modes": {
"camera": "Instantâneo da câmera do Home Assistant, da entidade de câmera",
@@ -265,7 +268,6 @@
}
},
"draggable": "A visualização ao vivo das câmeras pode ser arrastada/deslizada",
"layout": "layout",
"lazy_load": "As câmeras ao vivo são carregadas lentamente",
"lazy_unload": "As câmeras ao vivo são descarregadas preguiçosamente",
"microphone": {
@@ -289,7 +291,6 @@
"editor_label": "Controles do visualizador de mídia"
},
"draggable": "Visualizador de eventos pode ser arrastado/deslizado",
"layout": "Layout",
"lazy_load": "A mídia do Visualizador de eventos é carregada lentamente no carrossel",
"transition_effect": "Efeito de transição do Visualizador de eventos",
"transition_effects": {
@@ -441,8 +442,8 @@
"cameras": "Câmeras",
"cameras_secondary": "Câmeras para renderizar neste cartão",
"delete": "Excluir",
"dimensions": "Dimensões",
"dimensions_secondary": "Dimensões e opções de forma",
"dimensions": "",
"dimensions_secondary": "",
"image": "Imagem",
"image_secondary": "Opções de visualização de imagem estática",
"live": "Ao vivo",
@@ -576,4 +577,4 @@
},
"select_date": "Selecionar a data"
}
}
}
+11
View File
@@ -3,6 +3,12 @@
--video-max-height: none;
}
// When the carousel is not part of a grid ensure its height matches its
// container.
:host(:not([grid-id])) {
height: 100%;
}
// If the carousel has an unselected attribute set on it, do not let the
// pointer interact (e.g. hover, scroll) with underlying elements. This is used
// when the carousel is part of a media-grid. Without this next/prev controls
@@ -13,6 +19,11 @@
}
.embla__slide {
// Center the content horizontally (for cases where the configured aspect
// ratio is a mismatch with the card).
display: flex;
justify-content: center;
height: 100%;
flex: 0 0 100%;
}
+1 -1
View File
@@ -1,7 +1,7 @@
:host {
display: block;
height: 100%;
width: 100;
width: 100%;
}
.hidden {
+23
View File
@@ -2,6 +2,7 @@ import differenceInHours from 'date-fns/differenceInHours';
import differenceInMinutes from 'date-fns/differenceInMinutes';
import differenceInSeconds from 'date-fns/differenceInSeconds';
import format from 'date-fns/format';
import { StyleInfo } from 'lit/directives/style-map';
import isEqual from 'lodash-es/isEqual';
import mergeWith from 'lodash-es/mergeWith';
import { FrigateCardError } from '../types';
@@ -245,3 +246,25 @@ export const getChildrenFromElement = (parent: HTMLElement): HTMLElement[] => {
export const recursivelyMergeObjectsNotArrays = <T>(src1: T, src2: T): T => {
return mergeWith({}, src1, src2, (_a, b) => (Array.isArray(b) ? b : undefined));
};
export const aspectRatioToString = (options?: {
ratio?: number[];
defaultStatic?: boolean;
}): string => {
if (options?.ratio && options.ratio.length === 2) {
return `${options.ratio[0]} / ${options.ratio[1]}`;
} else if (options?.defaultStatic) {
return '16 / 9';
} else {
return 'auto';
}
};
export const aspectRatioToStyle = (options?: {
ratio?: number[];
defaultStatic?: boolean;
}): StyleInfo => {
return {
'aspect-ratio': aspectRatioToString(options),
};
};