diff --git a/.vscode/i18n-ally-reviews.yml b/.vscode/i18n-ally-reviews.yml
index 5b4628b2..3daa9fde 100644
--- a/.vscode/i18n-ally-reviews.yml
+++ b/.vscode/i18n-ally-reviews.yml
@@ -29,3 +29,99 @@ reviews:
Mostrar imagem estática enquanto a transmissão ao vivo está
carregando
time: '2022-08-07T23:00:35.117Z'
+ config.common.layout.fit:
+ locales:
+ it:
+ translation_candidate:
+ source: en
+ text: Disposizione adatta
+ time: '2022-08-12T03:00:50.438Z'
+ pt-BR:
+ translation_candidate:
+ source: en
+ text: Ajuste de layout
+ time: '2022-08-12T03:00:54.209Z'
+ config.common.layout.fits.contain:
+ locales:
+ it:
+ translation_candidate:
+ source: en
+ text: I media sono contenuti/incartati
+ time: '2022-08-12T03:01:00.885Z'
+ pt-BR:
+ translation_candidate:
+ source: en
+ text: A mídia está contida/em letterbox
+ time: '2022-08-12T03:01:03.607Z'
+ config.common.layout.fits.cover:
+ locales:
+ it:
+ translation_candidate:
+ source: en
+ text: Il supporto si espande proporzionalmente per coprire la scheda
+ time: '2022-08-12T03:01:09.001Z'
+ pt-BR:
+ translation_candidate:
+ source: en
+ text: A mídia se expande proporcionalmente para cobrir o cartão
+ time: '2022-08-12T03:01:11.240Z'
+ config.common.layout.fits.fill:
+ locales:
+ it:
+ translation_candidate:
+ source: en
+ text: Il supporto viene allungato per riempire la scheda
+ time: '2022-08-12T03:01:14.657Z'
+ pt-BR:
+ translation_candidate:
+ source: en
+ text: A mídia é esticada para preencher o cartão
+ time: '2022-08-12T03:01:17.319Z'
+ config.common.layout.position.x:
+ locales:
+ it:
+ translation_candidate:
+ source: en
+ text: Percentuale di posizionamento orizzontale
+ time: '2022-08-12T03:01:20.714Z'
+ pt-BR:
+ translation_candidate:
+ source: en
+ text: Porcentagem de posicionamento horizontal
+ time: '2022-08-12T03:01:22.619Z'
+ config.common.layout.position.y:
+ locales:
+ it:
+ translation_candidate:
+ source: en
+ text: Percentuale di posizionamento verticale
+ time: '2022-08-12T03:01:26.332Z'
+ pt-BR:
+ translation_candidate:
+ source: en
+ text: Porcentagem de posicionamento vertical
+ time: '2022-08-12T03:01:28.398Z'
+ config.image.layout:
+ locales:
+ it:
+ translation_candidate:
+ source: en
+ text: Disposizione dell'immagine
+ time: '2022-08-12T03:01:33.020Z'
+ pt-BR:
+ translation_candidate:
+ source: en
+ text: Esquema de imagem
+ time: '2022-08-12T03:01:34.987Z'
+ config.media_viewer.layout:
+ locales:
+ it:
+ translation_candidate:
+ source: en
+ text: Layout del visualizzatore multimediale
+ time: '2022-08-12T03:01:45.033Z'
+ pt-BR:
+ translation_candidate:
+ source: en
+ text: Layout do visualizador de mídia
+ time: '2022-08-12T03:01:46.528Z'
diff --git a/README.md b/README.md
index 04dae47d..71a81f71 100644
--- a/README.md
+++ b/README.md
@@ -332,6 +332,7 @@ See the [fully expanded live configuration example](#config-expanded-live) for h
| `controls` | | :white_check_mark: | Configuration for the `live` view controls. See below. |
| `jsmpeg` | | :white_check_mark: | Configuration for the `frigate-jsmpeg` live provider. See below.|
| `webrtc_card` | | :white_check_mark: | Configuration for the `webrtc-card` live provider. See below.|
+| `layout` | | :white_check_mark: | See [media layout](#media-layout) below.|
#### Live Provider: JSMPEG Configuration
@@ -437,6 +438,7 @@ 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: Next / Previous
@@ -468,8 +470,8 @@ media_viewer:
| `mode` | `none` | :heavy_multiplication_x: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).|
| `size` | 100 | :heavy_multiplication_x: | The size of the thumbnails in the thumbnail carousel pixels. Must be >= `75` and <= `175`.|
| `show_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
-| `show_favorite_control` | `true` | :white_check_mark: | Whether to show the favorite ('star') control on each thumbnail.|
-| `show_timeline_control` | `true` | :white_check_mark: | Whether to show the timeline ('target') control on each thumbnail.|
+| `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.|
#### Media Viewer Controls: Title
@@ -502,8 +504,8 @@ See the [fully expanded event gallery configuration example](#config-expanded-ev
| - | - | - | - |
| `size` | 100 | :heavy_multiplication_x: | The size of the thumbnails in the event gallery in pixels. Must be >= `75` and <= `175`.|
| `show_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
-| `show_favorite_control` | `true` | :white_check_mark: | Whether to show the favorite ('star') control on each thumbnail.|
-| `show_timeline_control` | `true` | :white_check_mark: | Whether to show the timeline ('target') control on each thumbnail.|
+| `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.|
| `actions` | | :heavy_multiplication_x: | Actions to use for all views that use the `event_gallery` (e.g. `clips`, `snapshots`). See [actions](#actions) below.|
### Image Options
@@ -565,6 +567,8 @@ timeline:
| `show_favorite_control` | `true` | :white_check_mark: | Whether to show the favorite ('star') control on each thumbnail.|
| `show_timeline_control` | `true` | :white_check_mark: | Whether to show the timeline ('target') control on each thumbnail.|
+
+
### Dimensions Options
These options control the aspect-ratio of the entire card to make placement in
@@ -583,8 +587,8 @@ See the [fully expanded dimensions configuration example](#config-expanded-dimen
| Option | Default | Overridable | Description |
| - | - | - | - |
-| `aspect_ratio_mode` | `dynamic` | :heavy_multiplication_x: | The aspect ratio mode to use. Acceptable values: `dynamic`, `static`, `unconstrained`. See [aspect ratios](#aspect-ratios) below.|
-| `aspect_ratio` | `16:9` | :heavy_multiplication_x: | The aspect ratio to use. Acceptable values: `:` or `/`. See [aspect ratios](#aspect-ratios) below.|
+| `aspect_ratio_mode` | `dynamic` | :white_check_mark: | The aspect ratio mode to use. Acceptable values: `dynamic`, `static`, `unconstrained`. See [aspect ratios](#aspect-ratios) below.|
+| `aspect_ratio` | `16:9` | :white_check_mark: | The aspect ratio to use. Acceptable values: `:` or `/`. See [aspect ratios](#aspect-ratios) below.|
#### `dimensions.aspect_ratio_mode`:
@@ -615,6 +619,8 @@ The card aspect ratio can be changed with the `dimensions.aspect_ratio_mode` and
If no aspect ratio is specified or available, but one is needed then `16:9` will
be used by default.
+
+
### Overrides Options
All configuration is a list under:
@@ -643,6 +649,34 @@ item, that has both of the following parameters set:
| `conditions` | | :heavy_multiplication_x: | A set of conditions that must evaluate to `true` in order for the overrides to be applied. See [Frigate Card Conditions](#frigate-card-conditions). |
| `overrides` | | :heavy_multiplication_x: |Configuration overrides to be applied. Any configuration parameter described in this documentation as 'Overridable' is supported. |
+
+
+### 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).
+
### Using AlexxIT's WebRTC Card
@@ -1281,6 +1315,11 @@ live:
title:
mode: popup-bottom-right
duration_seconds: 2
+ layout:
+ fit: contain
+ position:
+ x: 50
+ y: 50
actions:
entity: light.office_main_lights
tap_action:
@@ -1326,6 +1365,11 @@ media_viewer:
title:
mode: popup-bottom-right
duration_seconds: 2
+ layout:
+ fit: contain
+ position:
+ x: 50
+ y: 50
actions:
entity: light.office_main_lights
tap_action:
@@ -1382,6 +1426,11 @@ Reference: [Image Options](#image-options).
image:
mode: url
refresh_seconds: 0
+ layout:
+ fit: contain
+ position:
+ x: 50
+ y: 50
actions:
entity: light.office_main_lights
tap_action:
@@ -2598,6 +2647,95 @@ view:
```
+
+
+### Changing the Media Layout
+
+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.
+
+```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
+```
+
+
+
+ Expand: Convert a landscape camera to a portrait live view
+
+Take the left-hand side (position with x == `0` and use that as the basis of a `9:16` (i.e. portrait) live view.
+
+```yaml
+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
+```
+
+
+
## Card Refreshes
diff --git a/src/components/image.ts b/src/components/image.ts
index a2aa6770..b1ffb6f8 100644
--- a/src/components/image.ts
+++ b/src/components/image.ts
@@ -17,7 +17,8 @@ import { localize } from '../localize/localize.js';
import imageStyle from '../scss/image.scss';
import { CameraConfig, ImageViewConfig } from '../types.js';
import { isHassDifferent } from '../utils/ha';
-import { dispatchMediaShowEvent } from '../utils/media-info.js';
+import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
+import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
import { View } from '../view.js';
import { dispatchErrorMessageEvent } from './message.js';
@@ -99,6 +100,7 @@ export class FrigateCardImage extends LitElement {
this._getImageSource.bind(this),
);
}
+ updateElementStyleFromMediaLayoutConfig(this, this.imageConfig?.layout);
}
// If the camera or view changed, immediately discard the old value (view to
@@ -228,7 +230,7 @@ export class FrigateCardImage extends LitElement {
${ref(this._refImage)}
src=${live(src)}
@load=${(ev: Event) => {
- dispatchMediaShowEvent(this, ev);
+ dispatchMediaLoadedEvent(this, ev);
}}
@error=${() => {
if (this.imageConfig?.mode === 'camera') {
diff --git a/src/components/live.ts b/src/components/live.ts
index 2e7be0ed..bb8fbbb5 100644
--- a/src/components/live.ts
+++ b/src/components/live.ts
@@ -47,7 +47,7 @@ import { homeAssistantSignPath } from '../utils/ha';
import { getFullDependentBrowseMediaQueryParameters } from '../utils/ha/browse-media.js';
import {
dispatchExistingMediaLoadedInfoAsEvent,
- dispatchMediaShowEvent,
+ dispatchMediaLoadedEvent,
dispatchMediaUnloadedEvent,
} from '../utils/media-info.js';
import { dispatchViewContextChangeEvent, View } from '../view.js';
@@ -67,6 +67,7 @@ import { EmblaCarouselPlugins } from './carousel.js';
import { renderTask } from '../utils/task.js';
import { classMap } from 'lit/directives/class-map.js';
import './image';
+import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
// Number of seconds a signed URL is valid for.
const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
@@ -751,6 +752,9 @@ export class FrigateCardLiveProvider extends LitElement {
dispatchMediaUnloadedEvent(this);
}
}
+ if (changedProps.has('liveConfig')) {
+ updateElementStyleFromMediaLayoutConfig(this, this.liveConfig?.layout);
+ }
}
/**
@@ -1080,7 +1084,7 @@ export class FrigateCardLiveWebRTCCard extends LitElement {
if (onloadeddata) {
onloadeddata.call(video, e);
}
- dispatchMediaShowEvent(this, video);
+ dispatchMediaLoadedEvent(this, video);
};
}
});
@@ -1219,7 +1223,7 @@ export class FrigateCardLiveJSMPEG extends LitElement {
// ignore any subsequent calls.
if (!videoDecoded && this._jsmpegCanvasElement) {
videoDecoded = true;
- dispatchMediaShowEvent(this, this._jsmpegCanvasElement);
+ dispatchMediaLoadedEvent(this, this._jsmpegCanvasElement);
resolve(player);
}
},
diff --git a/src/components/viewer.ts b/src/components/viewer.ts
index 44d54b6c..6518a123 100644
--- a/src/components/viewer.ts
+++ b/src/components/viewer.ts
@@ -54,6 +54,7 @@ import '../patches/ha-hls-player';
import './surround-thumbnails';
import { EmblaCarouselPlugins } from './carousel.js';
import { renderTask } from '../utils/task.js';
+import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
@customElement('frigate-card-viewer')
export class FrigateCardViewer extends LitElement {
@@ -573,6 +574,16 @@ export class FrigateCardViewerCarousel extends LitElement {
return true;
}
+ /**
+ * Called when an update will occur.
+ * @param changedProps The changed properties
+ */
+ protected willUpdate(changedProps: PropertyValues): void {
+ if (changedProps.has('viewerConfig')) {
+ updateElementStyleFromMediaLayoutConfig(this, this.viewerConfig?.layout);
+ }
+ }
+
/**
* Render the element, resolving the media first if necessary.
*/
@@ -664,6 +675,12 @@ export class FrigateCardViewerCarousel extends LitElement {
}
}
+ /**
+ * Render a single media item in the viewer carousel.
+ * @param mediaToRender The FrigateBrowseMediaSource to render.
+ * @param slideIndex The index of the slide to render.
+ * @returns A rendered template.
+ */
protected _renderMediaItem(
mediaToRender: FrigateBrowseMediaSource,
slideIndex: number,
diff --git a/src/const.ts b/src/const.ts
index 9e404fb3..ca56f40e 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -88,6 +88,11 @@ 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;
export const CONF_LIVE = 'live' as const;
export const CONF_LIVE_AUTO_PLAY = `${CONF_LIVE}.auto_play` as const;
@@ -113,16 +118,23 @@ export const CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL =
export const CONF_LIVE_CONTROLS_TITLE_MODE = `${CONF_LIVE}.controls.title.mode` as const;
export const CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS =
`${CONF_LIVE}.controls.title.duration_seconds` 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_JSMPEG = `${CONF_LIVE}.jsmpeg` 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;
export const CONF_LIVE_PRELOAD = `${CONF_LIVE}.preload` as const;
export const CONF_LIVE_TRANSITION_EFFECT = `${CONF_LIVE}.transition_effect` as const;
-export const CONF_LIVE_SHOW_IMAGE_DURING_LOAD = `${CONF_LIVE}.show_image_during_load` as const;
+export const CONF_LIVE_SHOW_IMAGE_DURING_LOAD =
+ `${CONF_LIVE}.show_image_during_load` as const;
export const CONF_LIVE_WEBRTC_CARD = `${CONF_LIVE}.webrtc_card` as const;
export 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 996028db..b5f92116 100644
--- a/src/editor.ts
+++ b/src/editor.ts
@@ -36,6 +36,9 @@ import {
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SIZE,
+ CONF_IMAGE_LAYOUT_FIT,
+ CONF_IMAGE_LAYOUT_POSITION_X,
+ CONF_IMAGE_LAYOUT_POSITION_Y,
CONF_IMAGE_MODE,
CONF_IMAGE_REFRESH_SECONDS,
CONF_IMAGE_URL,
@@ -54,6 +57,9 @@ import {
CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS,
CONF_LIVE_CONTROLS_TITLE_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_PRELOAD,
@@ -73,6 +79,9 @@ import {
CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS,
CONF_MEDIA_VIEWER_CONTROLS_TITLE_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_TRANSITION_EFFECT,
CONF_MENU_ALIGNMENT,
@@ -125,8 +134,11 @@ const MENU_CAMERAS_FRIGATE = 'cameras.frigate';
const MENU_CAMERAS_TRIGGERS = 'cameras.triggers';
const MENU_CAMERAS_WEBRTC = 'cameras.webrtc';
const MENU_EVENT_GALLERY_CONTROLS = 'event_gallery.controls';
+const MENU_IMAGE_LAYOUT = 'image.layout';
const MENU_LIVE_CONTROLS = 'live.controls';
+const MENU_LIVE_LAYOUT = 'live.layout';
const MENU_MEDIA_VIEWER_CONTROLS = 'media_viewer.controls';
+const MENU_MEDIA_VIEWER_LAYOUT = 'media_viewer.layout';
const MENU_TIMELINE_CONTROLS = 'timeline.controls';
const MENU_OPTIONS = 'options';
const MENU_VIEW_SCAN = 'scan';
@@ -402,6 +414,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
{ value: 'never', label: localize('config.common.media_action_conditions.never') },
];
+ 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') },
+ ];
+
public setConfig(config: RawFrigateCardConfig): void {
// Note: This does not use Zod to parse the configuration, so it may be
// partially or completely invalid. It's more useful to have a partially
@@ -763,6 +782,43 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
`;
}
+ /**
+ * Render a media layout section.
+ * @param domain The submenu domain.
+ * @param labelPath The path to the label.
+ * @param configPathFit The path to the fit config.
+ * @param configPathPositionX The path to the position.x config.
+ * @param configPathPositionY The path to the position.y config.
+ * @returns A rendered template.
+ */
+ protected _renderMediaLayout(
+ domain: string,
+ labelPath: string,
+ configPathFit: string,
+ configPathPositionX: string,
+ configPathPositionY: string,
+ ): TemplateResult | void {
+ return this._putInSubmenu(
+ domain,
+ true,
+ labelPath,
+ { name: 'mdi:page-layout-body' },
+ html`
+ ${this._renderOptionSelector(configPathFit, this._layoutFits)}
+ ${this._renderNumberInput(configPathPositionX, {
+ min: 0,
+ max: 100,
+ label: localize('config.common.layout.position.x'),
+ })}
+ ${this._renderNumberInput(configPathPositionY, {
+ min: 0,
+ max: 100,
+ label: localize('config.common.layout.position.y'),
+ })}
+ `,
+ );
+ }
+
/**
* Render a camera section.
* @param cameras The full array of cameras.
@@ -1258,6 +1314,13 @@ 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,
+ )}
`
: ''}
@@ -1327,44 +1390,53 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
true,
'config.media_viewer.controls.options',
{ name: 'mdi:gamepad' },
- html` ${this._renderOptionSelector(
- CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
- this._eventViewerNextPreviousControlStyles,
- )}
- ${this._renderNumberInput(
- CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
- {
- min: BUTTON_SIZE_MIN,
- },
- )}
- ${this._renderOptionSelector(
- CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
- this._thumbnailModes,
- )}
- ${this._renderNumberInput(CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SIZE, {
- min: THUMBNAIL_WIDTH_MIN,
- max: THUMBNAIL_WIDTH_MAX,
- })}
- ${this._renderSwitch(
- CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
- defaults.media_viewer.controls.thumbnails.show_details,
- )}
- ${this._renderSwitch(
- CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
- defaults.media_viewer.controls.thumbnails.show_favorite_control,
- )}
- ${this._renderSwitch(
- CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
- defaults.media_viewer.controls.thumbnails.show_timeline_control,
- )}
- ${this._renderOptionSelector(
- CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
- this._titleModes,
- )}
- ${this._renderNumberInput(
- CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS,
- { min: 0, max: 60 },
- )}`,
+ html`
+ ${this._renderOptionSelector(
+ CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
+ this._eventViewerNextPreviousControlStyles,
+ )}
+ ${this._renderNumberInput(
+ CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
+ {
+ min: BUTTON_SIZE_MIN,
+ },
+ )}
+ ${this._renderOptionSelector(
+ CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
+ this._thumbnailModes,
+ )}
+ ${this._renderNumberInput(CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SIZE, {
+ min: THUMBNAIL_WIDTH_MIN,
+ max: THUMBNAIL_WIDTH_MAX,
+ })}
+ ${this._renderSwitch(
+ CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
+ defaults.media_viewer.controls.thumbnails.show_details,
+ )}
+ ${this._renderSwitch(
+ CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
+ defaults.media_viewer.controls.thumbnails.show_favorite_control,
+ )}
+ ${this._renderSwitch(
+ CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
+ defaults.media_viewer.controls.thumbnails.show_timeline_control,
+ )}
+ ${this._renderOptionSelector(
+ CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
+ this._titleModes,
+ )}
+ ${this._renderNumberInput(
+ CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS,
+ { min: 0, max: 60 },
+ )}
+ `,
+ )}
+ ${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,
)}
`
: ''}
@@ -1374,6 +1446,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderOptionSelector(CONF_IMAGE_MODE, this._imageModes)}
${this._renderStringInput(CONF_IMAGE_URL)}
${this._renderNumberInput(CONF_IMAGE_REFRESH_SECONDS)}
+ ${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 829435e7..edd0f093 100644
--- a/src/localize/languages/en.json
+++ b/src/localize/languages/en.json
@@ -56,6 +56,18 @@
"selected": "On selection",
"unselected": "On unselection",
"visible": "On browser/tab visibility"
+ },
+ "layout": {
+ "fit": "Layout fit",
+ "fits": {
+ "cover": "Media expands proportionally to cover the card",
+ "contain": "Media is contained/letterboxed",
+ "fill": "Media is stretched to fill the card"
+ },
+ "position": {
+ "x": "Horizontal placement percentage",
+ "y": "Vertical placement percentage"
+ }
}
},
"dimensions": {
@@ -79,6 +91,7 @@
}
},
"image": {
+ "layout": "Image Layout",
"mode": "Image view mode",
"modes": {
"camera": "Home Assistant camera snapshot of camera entity",
@@ -122,6 +135,7 @@
}
},
"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",
"preload": "Preload live view in the background",
@@ -172,6 +186,7 @@
},
"draggable": "Media Viewer can be dragged/swiped",
"lazy_load": "Media Viewer media is lazily loaded in carousel",
+ "layout": "Media Viewer Layout",
"transition_effect": "Media Viewer transition effect",
"transition_effects": {
"none": "No transition",
diff --git a/src/patches/ha-camera-stream.ts b/src/patches/ha-camera-stream.ts
index 8be48613..55e186bc 100644
--- a/src/patches/ha-camera-stream.ts
+++ b/src/patches/ha-camera-stream.ts
@@ -1,5 +1,5 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-nocheck
+//@ts-nocheck
// ====================================================================
// ** Keep modifications to this file to a minimum **
@@ -9,13 +9,14 @@
// available as compilation time.
// ====================================================================
-import { css, html, TemplateResult } from 'lit';
+import { css, CSSResultGroup, html, unsafeCSS, TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';
import { query } from 'lit/decorators/query.js';
import { FrigateCardMediaPlayer } from '../types.js';
-import { dispatchMediaShowEvent } from '../utils/media-info.js';
-import "./ha-hls-player";
-import "./ha-web-rtc-player";
+import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
+import './ha-hls-player';
+import './ha-web-rtc-player';
+import liveHAComponentsStyle from '../scss/live-ha-components.scss';
customElements.whenDefined('ha-camera-stream').then(() => {
// ========================================================================================
@@ -98,8 +99,8 @@ customElements.whenDefined('ha-camera-stream').then(() => {
if (this._shouldRenderMJPEG) {
return html`
{
- dispatchMediaShowEvent(this, e);
+ @load=${(ev: Event) => {
+ dispatchMediaLoadedEvent(this, ev);
}}
.src=${typeof this._connected == 'undefined' || this._connected
? computeMJPEGStreamUrl(this.stateObj)
@@ -138,6 +139,7 @@ customElements.whenDefined('ha-camera-stream').then(() => {
static get styles(): CSSResultGroup {
return [
super.styles,
+ unsafeCSS(liveHAComponentsStyle),
css`
:host {
width: 100%;
@@ -146,7 +148,6 @@ customElements.whenDefined('ha-camera-stream').then(() => {
img {
width: 100%;
height: 100%;
- object-fit: contain;
}
`,
];
@@ -155,7 +156,7 @@ customElements.whenDefined('ha-camera-stream').then(() => {
});
declare global {
- interface HTMLElementTagNameMap {
- "frigate-card-ha-camera-stream": FrigateCardHaCameraStream
- }
+ interface HTMLElementTagNameMap {
+ 'frigate-card-ha-camera-stream': FrigateCardHaCameraStream;
+ }
}
diff --git a/src/patches/ha-hls-player.ts b/src/patches/ha-hls-player.ts
index b5456155..fa910c91 100644
--- a/src/patches/ha-hls-player.ts
+++ b/src/patches/ha-hls-player.ts
@@ -9,11 +9,12 @@
// available as compilation time.
// ====================================================================
-import { css, html, TemplateResult } from 'lit';
+import { css, CSSResultGroup, html, unsafeCSS, TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';
import { query } from 'lit/decorators/query.js';
import { dispatchErrorMessageEvent } from '../components/message.js';
-import { dispatchMediaShowEvent } from '../utils/media-info.js';
+import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
+import liveHAComponentsStyle from '../scss/live-ha-components.scss';
customElements.whenDefined('ha-hls-player').then(() => {
@customElement('frigate-card-ha-hls-player')
@@ -85,7 +86,7 @@ customElements.whenDefined('ha-hls-player').then(() => {
?playsinline=${this.playsInline}
?controls=${this.controls}
@loadeddata=${(e) => {
- dispatchMediaShowEvent(this, e);
+ dispatchMediaLoadedEvent(this, e);
}}
>
`;
@@ -94,15 +95,15 @@ customElements.whenDefined('ha-hls-player').then(() => {
static get styles(): CSSResultGroup {
return [
super.styles,
+ unsafeCSS(liveHAComponentsStyle),
css`
:host {
width: 100%;
height: 100%;
}
video {
- object-fit: contain;
- height: 100%;
width: 100%;
+ height: 100%;
}
`,
];
diff --git a/src/patches/ha-web-rtc-player.ts b/src/patches/ha-web-rtc-player.ts
index 6a18242b..5c4a1d44 100644
--- a/src/patches/ha-web-rtc-player.ts
+++ b/src/patches/ha-web-rtc-player.ts
@@ -9,11 +9,12 @@
// available as compilation time.
// ====================================================================
-import { html, TemplateResult } from 'lit';
+import { css, CSSResultGroup, html, unsafeCSS, TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';
import { query } from 'lit/decorators/query.js';
import { dispatchErrorMessageEvent } from '../components/message.js';
-import { dispatchMediaShowEvent } from '../utils/media-info.js';
+import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
+import liveHAComponentsStyle from '../scss/live-ha-components.scss';
customElements.whenDefined('ha-web-rtc-player').then(() => {
@customElement('frigate-card-ha-web-rtc-player')
@@ -86,11 +87,28 @@ customElements.whenDefined('ha-web-rtc-player').then(() => {
?playsinline=${this.playsInline}
?controls=${this.controls}
@loadeddata=${(e) => {
- dispatchMediaShowEvent(this, e);
+ dispatchMediaLoadedEvent(this, e);
}}
>
`;
}
+
+ static get styles(): CSSResultGroup {
+ return [
+ super.styles,
+ unsafeCSS(liveHAComponentsStyle),
+ css`
+ :host {
+ width: 100%;
+ height: 100%;
+ }
+ video {
+ width: 100%;
+ height: 100%;
+ }
+ `,
+ ];
+ }
}
});
diff --git a/src/scss/const.scss b/src/scss/const.scss
index 8c612c78..4d531702 100644
--- a/src/scss/const.scss
+++ b/src/scss/const.scss
@@ -1,4 +1,6 @@
:host {
--frigate-card-thumbnail-size-max: 175px;
- --frigate-card-thumbnail-details-width: calc(var(--frigate-card-thumbnail-size) + 200px);
+ --frigate-card-thumbnail-details-width: calc(
+ var(--frigate-card-thumbnail-size) + 200px
+ );
}
\ No newline at end of file
diff --git a/src/scss/editor.scss b/src/scss/editor.scss
index 9144bd9b..0c251615 100644
--- a/src/scss/editor.scss
+++ b/src/scss/editor.scss
@@ -26,6 +26,11 @@
background: var(--secondary-background-color);
display: grid;
}
+// Space submenus from the next top-level options.
+.values + .option,
+.submenu + .option {
+ margin-top: 10px;
+}
div.upgrade {
width: auto;
border: 1px dotted var(--primary-color);
diff --git a/src/scss/image.scss b/src/scss/image.scss
index c8d401d5..954b500b 100644
--- a/src/scss/image.scss
+++ b/src/scss/image.scss
@@ -1,6 +1,9 @@
+@use 'media-layout.scss';
+
img {
width: 100%;
height: 100%;
display: block;
- object-fit: scale-down;
+
+ @include media-layout.media-layout();
}
\ No newline at end of file
diff --git a/src/scss/live-ha-components.scss b/src/scss/live-ha-components.scss
new file mode 100644
index 00000000..2815cb6a
--- /dev/null
+++ b/src/scss/live-ha-components.scss
@@ -0,0 +1,5 @@
+@use 'media-layout.scss';
+
+img, video {
+ @include media-layout.media-layout();
+}
\ No newline at end of file
diff --git a/src/scss/live-jsmpeg.scss b/src/scss/live-jsmpeg.scss
index 8adaa565..0cb0dfb6 100644
--- a/src/scss/live-jsmpeg.scss
+++ b/src/scss/live-jsmpeg.scss
@@ -1,3 +1,5 @@
+@use 'media-layout.scss';
+
:host {
width: 100%;
height: 100%;
@@ -8,5 +10,6 @@ canvas {
display: block;
width: 100%;
height: 100%;
- object-fit: contain;
-}
\ No newline at end of file
+
+ @include media-layout.media-layout();
+}
diff --git a/src/scss/live-webrtc.scss b/src/scss/live-webrtc.scss
index 9a0a1548..6482aed1 100644
--- a/src/scss/live-webrtc.scss
+++ b/src/scss/live-webrtc.scss
@@ -1,3 +1,5 @@
+@use 'media-layout.scss';
+
:host {
width: 100%;
height: 100%;
@@ -12,4 +14,8 @@
// To get the WebRTC player to line up correctly with some themes.
margin: 0px;
box-shadow: none;
-}
\ No newline at end of file
+}
+
+#webrtc #video {
+ @include media-layout.media-layout();
+}
diff --git a/src/scss/media-layout.scss b/src/scss/media-layout.scss
new file mode 100644
index 00000000..cde335ec
--- /dev/null
+++ b/src/scss/media-layout.scss
@@ -0,0 +1,5 @@
+@mixin media-layout {
+ 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%);
+}
diff --git a/src/scss/viewer-carousel.scss b/src/scss/viewer-carousel.scss
index 5e7f7390..fdf6042c 100644
--- a/src/scss/viewer-carousel.scss
+++ b/src/scss/viewer-carousel.scss
@@ -1,3 +1,5 @@
+@use 'media-layout.scss';
+
.embla__slide {
height: 100%;
flex: 0 0 100%;
@@ -8,7 +10,5 @@
width: 100%;
height: 100%;
- // Letterbox media. has similar added directly in
- // its element.
- object-fit: contain;
-}
\ No newline at end of file
+ @include media-layout.media-layout();
+}
diff --git a/src/types.ts b/src/types.ts
index c7c33be7..a7d4046c 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -165,9 +165,11 @@ const moreInfoActionSchema = schemaForType<
const customActionSchema = schemaForType<
CustomActionConfig & ExtendedConfirmationRestrictionConfig
>()(
- actionBaseSchema.extend({
- action: z.literal('fire-dom-event'),
- }).passthrough(),
+ actionBaseSchema
+ .extend({
+ action: z.literal('fire-dom-event'),
+ })
+ .passthrough(),
);
const noActionSchema = schemaForType<
NoActionConfig & ExtendedConfirmationRestrictionConfig
@@ -519,6 +521,18 @@ export type PictureElement = z.infer;
const pictureElementsSchema = pictureElementSchema.array().optional();
export type PictureElements = z.infer;
+/**
+ * Media layout configuration section.
+ */
+const mediaLayoutConfigSchema = z.object({
+ fit: z.enum(['contain', 'cover', 'fill']).optional(),
+ position: z.object({
+ x: z.number().min(0).max(100).optional(),
+ y: z.number().min(0).max(100).optional(),
+ }).optional(),
+});
+export type MediaLayoutConfig = z.infer;
+
/**
* View configuration section.
*/
@@ -580,6 +594,7 @@ const imageConfigSchema = z
mode: z.enum(IMAGE_MODES).default(imageConfigDefault.mode),
url: z.string().optional(),
refresh_seconds: z.number().min(0).default(imageConfigDefault.refresh_seconds),
+ layout: mediaLayoutConfigSchema.optional(),
})
.merge(actionsSchema)
.default(imageConfigDefault);
@@ -717,12 +732,14 @@ const liveOverridableConfigSchema = z
show_details: thumbnailsControlSchema.shape.show_details.default(
liveConfigDefault.controls.thumbnails.show_details,
),
- show_favorite_control: thumbnailsControlSchema.shape.show_favorite_control.default(
- liveConfigDefault.controls.thumbnails.show_favorite_control,
- ),
- show_timeline_control: thumbnailsControlSchema.shape.show_timeline_control.default(
- liveConfigDefault.controls.thumbnails.show_timeline_control,
- ),
+ show_favorite_control:
+ thumbnailsControlSchema.shape.show_favorite_control.default(
+ liveConfigDefault.controls.thumbnails.show_favorite_control,
+ ),
+ show_timeline_control:
+ thumbnailsControlSchema.shape.show_timeline_control.default(
+ liveConfigDefault.controls.thumbnails.show_timeline_control,
+ ),
media: z
.enum(['clips', 'snapshots'])
.default(liveConfigDefault.controls.thumbnails.media),
@@ -740,7 +757,10 @@ const liveOverridableConfigSchema = z
.default(liveConfigDefault.controls.title),
})
.default(liveConfigDefault.controls),
- show_image_during_load: z.boolean().default(liveConfigDefault.show_image_during_load),
+ show_image_during_load: z
+ .boolean()
+ .default(liveConfigDefault.show_image_during_load),
+ layout: mediaLayoutConfigSchema.optional(),
})
.merge(actionsSchema);
@@ -917,12 +937,14 @@ const viewerConfigSchema = z
show_details: thumbnailsControlSchema.shape.show_details.default(
viewerConfigDefault.controls.thumbnails.show_details,
),
- show_favorite_control: thumbnailsControlSchema.shape.show_favorite_control.default(
- viewerConfigDefault.controls.thumbnails.show_favorite_control,
- ),
- show_timeline_control: thumbnailsControlSchema.shape.show_timeline_control.default(
- viewerConfigDefault.controls.thumbnails.show_timeline_control,
- ),
+ show_favorite_control:
+ thumbnailsControlSchema.shape.show_favorite_control.default(
+ viewerConfigDefault.controls.thumbnails.show_favorite_control,
+ ),
+ show_timeline_control:
+ thumbnailsControlSchema.shape.show_timeline_control.default(
+ viewerConfigDefault.controls.thumbnails.show_timeline_control,
+ ),
})
.default(viewerConfigDefault.controls.thumbnails),
title: titleControlConfigSchema
@@ -937,6 +959,7 @@ const viewerConfigSchema = z
.default(viewerConfigDefault.controls.title),
})
.default(viewerConfigDefault.controls),
+ layout: mediaLayoutConfigSchema.optional(),
})
.merge(actionsSchema)
.default(viewerConfigDefault);
@@ -970,12 +993,14 @@ const galleryConfigSchema = z
show_details: thumbnailsControlSchema.shape.show_details.default(
galleryConfigDefault.controls.thumbnails.show_details,
),
- show_favorite_control: thumbnailsControlSchema.shape.show_favorite_control.default(
- galleryConfigDefault.controls.thumbnails.show_favorite_control,
- ),
- show_timeline_control: thumbnailsControlSchema.shape.show_timeline_control.default(
- galleryConfigDefault.controls.thumbnails.show_timeline_control,
- ),
+ show_favorite_control:
+ thumbnailsControlSchema.shape.show_favorite_control.default(
+ galleryConfigDefault.controls.thumbnails.show_favorite_control,
+ ),
+ show_timeline_control:
+ thumbnailsControlSchema.shape.show_timeline_control.default(
+ galleryConfigDefault.controls.thumbnails.show_timeline_control,
+ ),
})
.default(galleryConfigDefault.controls.thumbnails),
})
@@ -1062,12 +1087,14 @@ const timelineConfigSchema = z
show_details: thumbnailsControlSchema.shape.show_details.default(
timelineConfigDefault.controls.thumbnails.show_details,
),
- show_favorite_control: thumbnailsControlSchema.shape.show_favorite_control.default(
- timelineConfigDefault.controls.thumbnails.show_favorite_control,
- ),
- show_timeline_control: thumbnailsControlSchema.shape.show_timeline_control.default(
- timelineConfigDefault.controls.thumbnails.show_timeline_control,
- ),
+ show_favorite_control:
+ thumbnailsControlSchema.shape.show_favorite_control.default(
+ timelineConfigDefault.controls.thumbnails.show_favorite_control,
+ ),
+ show_timeline_control:
+ thumbnailsControlSchema.shape.show_timeline_control.default(
+ timelineConfigDefault.controls.thumbnails.show_timeline_control,
+ ),
})
.default(timelineConfigDefault.controls.thumbnails),
})
@@ -1086,6 +1113,7 @@ const overrideConfigurationSchema = z.object({
menu: deepRemoveDefaults(menuConfigSchema).optional(),
image: deepRemoveDefaults(imageConfigSchema).optional(),
view: deepRemoveDefaults(viewConfigSchema).optional(),
+ dimensions: deepRemoveDefaults(dimensionsConfigSchema).optional(),
});
export type OverrideConfigurationKey = keyof z.infer;
@@ -1213,13 +1241,13 @@ export const MESSAGE_TYPE_PRIORITIES = {
error: 20,
connection: 30,
diagnostics: 40,
-}
+};
export type MessageType = 'info' | 'error' | 'connection' | 'diagnostics';
export interface Message {
message: string;
- type: MessageType,
+ type: MessageType;
icon?: string;
context?: unknown;
dotdotdot?: boolean;
diff --git a/src/utils/media-info.ts b/src/utils/media-info.ts
index f7fcee69..e642bece 100644
--- a/src/utils/media-info.ts
+++ b/src/utils/media-info.ts
@@ -43,7 +43,7 @@ export function createMediaLoadedInfo(
* @param element The element to send the event.
* @param source An event or HTMLElement that should be used as a source.
*/
-export function dispatchMediaShowEvent(
+export function dispatchMediaLoadedEvent(
element: HTMLElement,
source: Event | HTMLElement,
): void {
diff --git a/src/utils/media-layout.ts b/src/utils/media-layout.ts
new file mode 100644
index 00000000..bf5ff24f
--- /dev/null
+++ b/src/utils/media-layout.ts
@@ -0,0 +1,27 @@
+import { MediaLayoutConfig } from '../types';
+
+/**
+ * Update element style from a media configuration.
+ * @param element The element to update the style for.
+ * @param mediaLayoutConfig The media config object.
+ */
+export const updateElementStyleFromMediaLayoutConfig = (
+ element: HTMLElement,
+ mediaLayoutConfig?: MediaLayoutConfig,
+) => {
+ if (mediaLayoutConfig?.fit !== undefined) {
+ element.style.setProperty('--frigate-card-media-layout-fit', mediaLayoutConfig.fit);
+ } else {
+ element.style.removeProperty('--frigate-card-media-layout-fit');
+ }
+ for (const dimension of ['x', 'y']) {
+ if (mediaLayoutConfig?.position?.[dimension] !== undefined) {
+ element.style.setProperty(
+ `--frigate-card-media-layout-position-${dimension}`,
+ `${mediaLayoutConfig.position[dimension]}%`,
+ );
+ } else {
+ element.style.removeProperty(`--frigate-card-media-layout-position-${dimension}`);
+ }
+ }
+};