diff --git a/README.md b/README.md
index bb8359d3..88360996 100644
--- a/README.md
+++ b/README.md
@@ -146,6 +146,7 @@ See the [fully expanded cameras configuration example](#config-expanded-cameras)
| `triggers` | | :white_check_mark: | Define what should cause this camera to update/trigger. See [camera triggers](#camera-trigger-configuration) below. |
| `webrtc_card` | | :white_check_mark: | The WebRTC entity/URL to use for this camera with the `webrtc-card` live provider. See below. |
| `cast` | | :white_check_mark: | Configuration that controls how this camera is "casted" / sent to media players. See below. |
+| `dimensions` | | :white_check_mark: | Controls the dimensions and layout for media from this camera. See below. |
@@ -363,6 +364,39 @@ cameras:
| `dashboard_path` | | :white_check_mark: | A required field that specifies the name of the dashboard to cast. You can see this name in your HA URL when you visit the dashboard. |
| `view_path` | | :white_check_mark: | A required field that specifies view/"tab" on that dashboard to cast. This is the value you have specified in the `url` field of the view configuration on the dashboard. |
+### Camera Dimensions Configuration
+
+The `dimensions` block configures the dimensions and layout of media from a given camera.
+
+```yaml
+cameras:
+ - dimensions:
+```
+
+| Option | Default | Overridable | Description |
+| - | - | - | - |
+| `aspect_ratio` | | :white_check_mark: | An optional aspect ratio for media from this camera which will be used in `live` or media viewer related views (e.g. `clip`, `snapshot` and `recording`). Format is the same as the parameter of the same name under the [dimensions block](#dimensions) (which controls dimensions for the whole card), e.g. `16 / 9`. |
+| `layout` | | :white_check_mark: | How the media should be laid out *within* the camera dimensions. See below. |
+
+### Camera Dimensions Layout Configuration
+
+The `layout` block configures the dimensions and layout of media from a given camera.
+
+```yaml
+cameras:
+ - dimensions:
+ layout:
+```
+
+This block is used to control the fit and position of the media _within_ the camera dimensions (in order to control the dimensions for the whole card see [the card dimensions parameter](#dimensions) ). As the default behavior is to always expand to fit the media precisely, these options only make sense if the camera `dimensions.aspect_ratio` is set to a static value that forces a particular aspect ratio that does not match the camera media.
+
+| Option | Default | Overridable | Description |
+| - | - | - | - |
+| `fit` | `contain` | :white_check_mark: | If `contain`, the media is contained within the card and letterboxed if necessary. If `cover`, the media is expanded proportionally (i.e. maintaining the media aspect ratio) until the card is fully covered. If `fill`, the media is stretched to fill the card (i.e. ignoring the media aspect ratio). See [CSS object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) for technical details and a visualization. |
+| `position` | | :white_check_mark: | A dictionary that contains an `x` and `y` percentage (`0` - `100`) to control the position of the media when the fit is `cover`. This can be effectively used to "pan" the media around. At any given time, only one of `x` and `y` will have an effect, depending on whether media width is larger than the card width (in which case `x` controls the position) or the media height is larger than the card height (in which case `y` controls the position). A value of `0` means maximally to the left or top of the media, a value of `100` means maximally to the right or bottom of the media. See [CSS object-position](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) for technical details and a visualization. |
+
+See [media layout examples](#media-layout-examples).
+
#### Camera IDs: Referring to cameras in card configuration
@@ -533,7 +567,6 @@ See the [fully expanded live configuration example](#config-expanded-live) for h
| `show_image_during_load` | `true` | :white_check_mark: | If `true`, during the initial stream load, the `image` live provider will be shown instead of the loading video stream. This still image will auto-refresh and is replaced with the live stream once loaded. |
| `actions` | | :white_check_mark: | Actions to use for the `live` view. See [actions](#actions) below.|
| `controls` | | :white_check_mark: | Configuration for the `live` view controls. See below. |
-| `layout` | | :white_check_mark: | See [media layout](#media-layout) below.|
| `microphone` | | :white_check_mark: | See [microphone](#microphone) below.|
| `display` | | :white_check_mark: | See [display](#live-display) below.|
@@ -721,7 +754,6 @@ See the [fully expanded Media viewer configuration example](#config-expanded-med
| `transition_effect` | `slide` | :heavy_multiplication_x: | Effect to apply as a transition between event media. Accepted values: `slide` or `none`. |
| `controls` | | :heavy_multiplication_x: | Configuration for the Media viewer controls. See below. |
| `actions` | | :heavy_multiplication_x: | Actions to use for all views that use the `media_viewer` (e.g. `clip`, `snapshot`). See [actions](#actions) below.|
-| `layout` | | :white_check_mark: | See [media layout](#media-layout) below.|
#### Media Viewer Controls
@@ -939,8 +971,6 @@ timeline:
| `show_favorite_control` | `true` | :heavy_multiplication_x: | Whether to show the favorite ('star') control on each thumbnail.|
| `show_timeline_control` | `true` | :heavy_multiplication_x: | Whether to show the timeline ('target') control on each thumbnail.|
-
-
#### Timeline Seek Behavior
The behavior of the timeline during seeking/dragging can be controlled by means of the icon on the bottom-right of the timeline.
@@ -952,6 +982,8 @@ The behavior of the timeline during seeking/dragging can be controlled by means
| | Dragging the timeline will seek / select across all available media from the selected camera only, selecting the media item with the longest duration. |
| | Dragging the timeline will pan only without selected or seeking any media. |
+
+
### Dimensions Options
These options control the aspect-ratio of the entire card to make placement in
@@ -961,7 +993,6 @@ media. This only applies to the card in normal render mode -- when in
fullscreen, or when in expanded (popup/dialog mode) the aspect ratio is chosen
dynamically to maximize the amount of content shown.
-
All configuration is under:
```yaml
@@ -1139,34 +1170,6 @@ automations:
| `actions` | | :heavy_multiplication_x: | An optional list of actions that will be run when the conditions evaluate `true`. Actions can be [stock Home Assistant actions](https://www.home-assistant.io/dashboards/actions/) or [Frigate card actions](#frigate-card-actions).|
| `actions_not` | | :heavy_multiplication_x: | An optional list of actions that will be run when the conditions evaluate `false`. Actions can be [stock Home Assistant actions](https://www.home-assistant.io/dashboards/actions/) or [Frigate card actions](#frigate-card-actions).|
-
-
-### Media Layout
-
-The `live`, `media_viewer` and `image` sections all support `layout` option which is used to control the fit and position of the media _within_ the card dimensions (in order to control the card dimensions themselves see [the dimensions parameter](#dimensions) ).
-
-As the default card behavior is for the card to always expand to fit the media, these options only make sense if `dimensions.aspect_ratio_mode` is set to `static`.
-
-All configuration is under:
-
-```yaml
-live:
- layout:
-image:
- layout:
-media_viewer:
- layout:
-```
-
-| Option | Default | Overridable | Description |
-| - | - | - | - |
-| `fit` | `contain` | :white_check_mark: | If `contain`, the media is contained within the card and letterboxed if necessary. If `cover`, the media is expanded proportionally (i.e. maintaining the media aspect ratio) until the card is fully covered. If `fill`, the media is stretched to fill the card (i.e. ignoring the media aspect ratio). See [CSS object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) for technical details and a visualization. |
-| `position` | | :white_check_mark: | A dictionary that contains an `x` and `y` percentage (`0` - `100`) to control the position of the media when the fit is `cover`. This can be effectively used to "pan" the media around. At any given time, only one of `x` and `y` will have an effect, depending on whether media width is larger than the card width (in which case `x` controls the position) or the media height is larger than the card height (in which case `y` controls the position). A value of `0` means maximally to the left or top of the media, a value of `100` means maximally to the right or bottom of the media. See [CSS object-position](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) for technical details and a visualization. |
-
-If multiple cameras are configured in the card, use [overrides](#overrides) to configure different values per camera.
-
-See [media layout examples](#media-layout-examples).
-
### Other Options
@@ -1782,6 +1785,13 @@ cameras:
- binary_sensor.front_door_sensor
cast:
method: standard
+ dimensions:
+ aspect_ratio: 16:9
+ layout:
+ fit: contain
+ position:
+ x: 50
+ y: 50
- camera_entity: camera.entrance
live_provider: webrtc-card
engine: auto
@@ -2114,11 +2124,6 @@ live:
title:
mode: popup-bottom-right
duration_seconds: 2
- layout:
- fit: contain
- position:
- x: 50
- y: 50
microphone:
always_connected: false
disconnect_seconds: 90
@@ -2212,11 +2217,6 @@ media_viewer:
title:
mode: popup-bottom-right
duration_seconds: 2
- layout:
- fit: contain
- position:
- x: 50
- y: 50
display:
mode: single
grid_selected_width_factor: 2
@@ -2281,11 +2281,6 @@ image:
mode: url
refresh_seconds: 0
zoomable: true
- layout:
- fit: contain
- position:
- x: 50
- y: 50
actions:
entity: light.office_main_lights
tap_action:
@@ -3738,20 +3733,16 @@ Change how the media fits and is positioned within the card dimensions.
Expand: Stretching an image
-Stretch the stock Frigate card image into a 4:4 square.
+Stretch a camera into a 4:4 square.
```yaml
type: custom:frigate-card
cameras:
- camera_entity: camera.landing
-view:
- default: image
-dimensions:
- aspect_ratio_mode: static
- aspect_ratio: '4:4'
-image:
- layout:
- fit: fill
+ dimensions:
+ aspect_ratio: '4:4'
+ layout:
+ fit: fill
```
@@ -3764,56 +3755,12 @@ Take the left-hand side (position with x == `0` and use that as the basis of a `
type: custom:frigate-card
cameras:
- camera_entity: camera.landing
-dimensions:
- aspect_ratio_mode: static
- aspect_ratio: '9:16'
-live:
- layout:
- fit: cover
- position:
- x: 0
-```
-
-
-
- Expand: Complex example to vary the media layouts for different cameras
-
-Configure three cameras (all natively landscape): the first uses automatic expansion of dimensions (the default), the second uses a portrait (`9:16`) card focused on the left side (`x` == `0`) of the media, the third uses a portrait (`9:16`) card focused on the right side (`x` == `100`) of the media.
-
-```yaml
-type: custom:frigate-card
-cameras:
- - camera_entity: camera.landing
- - camera_entity: camera.living_room
- - camera_entity: camera.sitting_room
-overrides:
- - conditions:
- camera:
- - camera.living_room
- - camera.sitting_room
- overrides:
- dimensions:
- aspect_ratio_mode: static
- aspect_ratio: '9:16'
- live:
- layout:
- fit: cover
- - conditions:
- camera:
- - camera.living_room
- overrides:
- live:
- layout:
- position:
- x: 0
- - conditions:
- camera:
- - camera.sitting_room
- overrides:
- live:
- layout:
- position:
- x: 100
+ dimensions:
+ aspect_ratio: '9:16'
+ layout:
+ fit: cover
+ position:
+ x: 0
```
diff --git a/src/card-controller/style-manager.ts b/src/card-controller/style-manager.ts
index d6185d5e..1a5763de 100644
--- a/src/card-controller/style-manager.ts
+++ b/src/card-controller/style-manager.ts
@@ -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 });
}
}
diff --git a/src/card.ts b/src/card.ts
index 9370f2b1..e4973c99 100644
--- a/src/card.ts
+++ b/src/card.ts
@@ -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) =>
this._controller.getMessageManager().setMessageIfHigherPriority(ev.detail)}
@frigate-card:view:change=${(ev: CustomEvent) =>
diff --git a/src/components/elements.ts b/src/components/elements.ts
index d8d7b563..9e3adf1d 100644
--- a/src/components/elements.ts
+++ b/src/components/elements.ts
@@ -70,7 +70,7 @@ interface HuiConditionalElement extends HTMLElement {
@customElement('frigate-card-elements-core')
export class FrigateCardElementsCore extends LitElement {
@property({ attribute: false })
- public elements: PictureElements;
+ public elements?: PictureElements;
/**
* Need to ensure card re-renders when conditions change, hence having it as a
@@ -275,10 +275,10 @@ export class FrigateCardElementsConditional extends LitElement {
* Render the card.
*/
protected render(): TemplateResult | void {
- if (evaluateConditionViaEvent(this, this._config.conditions)) {
+ if (evaluateConditionViaEvent(this, this._config?.conditions)) {
return html`
`;
}
diff --git a/src/components/image.ts b/src/components/image.ts
index 5bdff165..0ba8bbcf 100644
--- a/src/components/image.ts
+++ b/src/components/image.ts
@@ -26,7 +26,6 @@ import {
dispatchMediaPauseEvent,
dispatchMediaPlayEvent,
} from '../utils/media-info.js';
-import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
import { View } from '../view/view.js';
import { dispatchErrorMessageEvent } from './message.js';
@@ -153,7 +152,6 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
() => dispatchMediaPauseEvent(this),
);
}
- updateElementStyleFromMediaLayoutConfig(this, this.imageConfig?.layout);
if (changedProps.has('imageConfig') && this.imageConfig?.zoomable) {
import('./zoomer.js');
diff --git a/src/components/live/live.ts b/src/components/live/live.ts
index 4f01c22e..38ab5c0b 100644
--- a/src/components/live/live.ts
+++ b/src/components/live/live.ts
@@ -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,
+ });
}
}
diff --git a/src/components/viewer.ts b/src/components/viewer.ts
index 1b1791ab..e37a7df0 100644
--- a/src/components/viewer.ts
+++ b/src/components/viewer.ts
@@ -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 {
diff --git a/src/config-mgmt.ts b/src/config-mgmt.ts
index dd13f89f..614ad149 100644
--- a/src/config-mgmt.ts
+++ b/src/config-mgmt.ts
@@ -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'),
];
diff --git a/src/config/types.ts b/src/config/types.ts
index 39d745cc..44bddb1d 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -524,19 +524,19 @@ export type FrigateConditional = z.infer;
// 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;
@@ -556,6 +556,21 @@ const mediaLayoutConfigSchema = z.object({
});
export type MediaLayoutConfig = z.infer;
+// *************************************************************************
+// 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;
// *************************************************************************
// 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;
@@ -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),
})
diff --git a/src/const.ts b/src/const.ts
index 5addcfae..e7c9e330 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -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;
diff --git a/src/editor.ts b/src/editor.ts
index 8c586f97..b9f9493a 100644
--- a/src/editor.ts
+++ b/src/editor.ts
@@ -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,
+ )}
+ `,
+ )}
`
: ``}
@@ -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,
- )}
`
: ''}
${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,
- )}
`
: ''}
${this._renderOptionSetHeader('timeline')}
diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json
index ee928903..6e41fb24 100644
--- a/src/localize/languages/en.json
+++ b/src/localize/languages/en.json
@@ -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",
diff --git a/src/localize/languages/it.json b/src/localize/languages/it.json
index 97558030..76f7fa6c 100644
--- a/src/localize/languages/it.json
+++ b/src/localize/languages/it.json
@@ -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"
}
-}
\ No newline at end of file
+}
diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json
index 3041dd2f..0243437f 100644
--- a/src/localize/languages/pt-BR.json
+++ b/src/localize/languages/pt-BR.json
@@ -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"
}
-}
\ No newline at end of file
+}
diff --git a/src/localize/languages/pt-PT.json b/src/localize/languages/pt-PT.json
index 78a11864..00d9b704 100644
--- a/src/localize/languages/pt-PT.json
+++ b/src/localize/languages/pt-PT.json
@@ -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"
}
-}
\ No newline at end of file
+}
diff --git a/src/scss/live-carousel.scss b/src/scss/live-carousel.scss
index 79c460f2..a6b9f29a 100644
--- a/src/scss/live-carousel.scss
+++ b/src/scss/live-carousel.scss
@@ -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%;
}
diff --git a/src/scss/live-provider.scss b/src/scss/live-provider.scss
index 3ac357c1..15c43662 100644
--- a/src/scss/live-provider.scss
+++ b/src/scss/live-provider.scss
@@ -1,7 +1,7 @@
:host {
display: block;
height: 100%;
- width: 100;
+ width: 100%;
}
.hidden {
diff --git a/src/utils/basic.ts b/src/utils/basic.ts
index a977e9e6..9c074f54 100644
--- a/src/utils/basic.ts
+++ b/src/utils/basic.ts
@@ -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 = (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),
+ };
+};
diff --git a/tests/card-controller/style-manager.test.ts b/tests/card-controller/style-manager.test.ts
index 475a3a9f..23a58bae 100644
--- a/tests/card-controller/style-manager.test.ts
+++ b/tests/card-controller/style-manager.test.ts
@@ -245,7 +245,7 @@ describe('StyleManager', () => {
it('without config or view', () => {
const api = createCardAPI();
const manager = new StyleManager(api);
- expect(manager.getAspectRatioStyle()).toBe('16 / 9');
+ expect(manager.getAspectRatioStyle()).toEqual({ 'aspect-ratio': '16 / 9' });
});
it('should be auto with unconstrained aspect ratio', () => {
@@ -260,7 +260,7 @@ describe('StyleManager', () => {
}),
);
const manager = new StyleManager(api);
- expect(manager.getAspectRatioStyle()).toBe('auto');
+ expect(manager.getAspectRatioStyle()).toEqual({ 'aspect-ratio': 'auto' });
});
it('should be auto in fullscreen', () => {
@@ -271,7 +271,7 @@ describe('StyleManager', () => {
vi.mocked(api.getFullscreenManager().isInFullscreen).mockReturnValue(true);
const manager = new StyleManager(api);
- expect(manager.getAspectRatioStyle()).toBe('auto');
+ expect(manager.getAspectRatioStyle()).toEqual({ 'aspect-ratio': 'auto' });
});
it('should be auto when expanded', () => {
@@ -282,7 +282,7 @@ describe('StyleManager', () => {
vi.mocked(api.getExpandManager().isExpanded).mockReturnValue(true);
const manager = new StyleManager(api);
- expect(manager.getAspectRatioStyle()).toBe('auto');
+ expect(manager.getAspectRatioStyle()).toEqual({ 'aspect-ratio': 'auto' });
});
it('should be auto when there is yet to be a view', () => {
@@ -291,7 +291,7 @@ describe('StyleManager', () => {
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
const manager = new StyleManager(api);
- expect(manager.getAspectRatioStyle()).toBe('auto');
+ expect(manager.getAspectRatioStyle()).toEqual({ 'aspect-ratio': 'auto' });
});
describe('should be auto when dynamic in certain views', () => {
@@ -317,7 +317,7 @@ describe('StyleManager', () => {
);
const manager = new StyleManager(api);
- expect(manager.getAspectRatioStyle()).toBe('auto');
+ expect(manager.getAspectRatioStyle()).toEqual({ 'aspect-ratio': 'auto' });
});
});
@@ -337,7 +337,7 @@ describe('StyleManager', () => {
);
const manager = new StyleManager(api);
- expect(manager.getAspectRatioStyle()).toBe('16 / 9');
+ expect(manager.getAspectRatioStyle()).toEqual({ 'aspect-ratio': '16 / 9' });
},
);
});
@@ -362,7 +362,9 @@ describe('StyleManager', () => {
});
const manager = new StyleManager(api);
- expect(manager.getAspectRatioStyle()).toBe('800 / 600');
+ expect(manager.getAspectRatioStyle()).toEqual({
+ 'aspect-ratio': '800 / 600',
+ });
},
);
});
@@ -381,7 +383,7 @@ describe('StyleManager', () => {
);
const manager = new StyleManager(api);
- expect(manager.getAspectRatioStyle()).toBe('4 / 3');
+ expect(manager.getAspectRatioStyle()).toEqual({ 'aspect-ratio': '4 / 3' });
});
});
});
diff --git a/tests/config-mgmt.test.ts b/tests/config-mgmt.test.ts
index 9ca34a98..1b3af522 100644
--- a/tests/config-mgmt.test.ts
+++ b/tests/config-mgmt.test.ts
@@ -1738,5 +1738,59 @@ describe('should handle version specific upgrades', () => {
});
});
});
+
+ describe('should handle media layout changes', () => {
+ it('from live.layout to camera_global.dimensions', () => {
+ const config = {
+ live: {
+ layout: {
+ fit: 'cover',
+ position: {
+ x: 42,
+ y: 43,
+ },
+ },
+ },
+ };
+ expect(upgradeConfig(config)).toBeTruthy();
+ expect(config).toEqual({
+ live: {},
+ cameras_global: {
+ dimensions: {
+ layout: {
+ fit: 'cover',
+ position: {
+ x: 42,
+ y: 43,
+ },
+ },
+ },
+ },
+ });
+ });
+
+ describe('from delete old media layouts', () => {
+ it.each([['media_viewer' as const], ['image' as const]])(
+ '%s',
+ (section: string) => {
+ const config = {
+ [section]: {
+ layout: {
+ fit: 'cover',
+ position: {
+ x: 42,
+ y: 43,
+ },
+ },
+ },
+ };
+ expect(upgradeConfig(config)).toBeTruthy();
+ expect(config).toEqual({
+ [section]: {},
+ });
+ },
+ );
+ });
+ });
});
});
diff --git a/tests/utils/basic.test.ts b/tests/utils/basic.test.ts
index 4961b777..4bbadfe6 100644
--- a/tests/utils/basic.test.ts
+++ b/tests/utils/basic.test.ts
@@ -2,8 +2,9 @@ import { afterAll, describe, expect, it, vi } from 'vitest';
import { FrigateCardError } from '../../src/types';
import {
allPromises,
- arrayMove,
arrayify,
+ arrayMove,
+ aspectRatioToStyle,
contentsChanged,
dayToDate,
dispatchFrigateCardEvent,
@@ -12,16 +13,16 @@ import {
formatDateAndTime,
getChildrenFromElement,
getDurationString,
- isHTMLElement,
isHoverableDevice,
+ isHTMLElement,
isSuperset,
isTruthy,
isValidDate,
prettifyTitle,
recursivelyMergeObjectsNotArrays,
runWhenIdleIfSupported,
- setOrRemoveAttribute,
setify,
+ setOrRemoveAttribute,
sleep,
} from '../../src/utils/basic';
import { createSlot, createSlotHost } from '../test-utils';
@@ -338,3 +339,20 @@ describe('recursivelyMergeObjectsNotArrays', () => {
});
});
});
+
+describe('aspectRatioToStyle', () => {
+ it('default', () => {
+ expect(aspectRatioToStyle()).toEqual({ 'aspect-ratio': 'auto' });
+ });
+ it('default static', () => {
+ expect(aspectRatioToStyle({ defaultStatic: true })).toEqual({
+ 'aspect-ratio': '16 / 9',
+ });
+ });
+ it('valid ratio', () => {
+ expect(aspectRatioToStyle({ ratio: [4, 3] })).toEqual({ 'aspect-ratio': '4 / 3' });
+ });
+ it('invalid ratio', () => {
+ expect(aspectRatioToStyle({ ratio: [4] })).toEqual({ 'aspect-ratio': 'auto' });
+ });
+});
diff --git a/vite.config.ts b/vite.config.ts
index ca89f07f..29404bb3 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -20,7 +20,6 @@ const FULL_COVERAGE_FILES_RELATIVE = [
'config-mgmt.ts',
'config/types.ts',
'const.ts',
- 'src/view/*.ts',
'types.ts',
'utils/action.ts',
'utils/audio.ts',
@@ -40,6 +39,7 @@ const FULL_COVERAGE_FILES_RELATIVE = [
'utils/substream.ts',
'utils/timer.ts',
'utils/zod.ts',
+ 'view/*.ts',
];
interface Threshold {
@@ -80,13 +80,13 @@ export default defineConfig({
// Expected thresholds for anything that does not have 100% coverage
// yet.
- statements: 15.54,
- branches: 10.38,
- functions: 16.18,
- lines: 15.84,
+ statements: 10.67,
+ branches: 4.9,
+ functions: 7.81,
+ lines: 10.92,
...calculateFullCoverageThresholds(),
},
},
},
-});
\ No newline at end of file
+});