Add ability to change media layout.
This commit is contained in:
Vendored
+96
@@ -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'
|
||||
|
||||
@@ -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.|
|
||||
|
||||
<a name="dimensions"></a>
|
||||
|
||||
### 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: `<W>:<H>` or `<W>/<H>`. 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: `<W>:<H>` or `<W>/<H>`. 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.
|
||||
|
||||
<a name="overrides"></a>
|
||||
|
||||
### 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. |
|
||||
|
||||
<a name="media-layout"></a>
|
||||
|
||||
### 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).
|
||||
|
||||
<a name="webrtc"></a>
|
||||
|
||||
### 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:
|
||||
```
|
||||
</details>
|
||||
|
||||
<a name="media-layout-examples"></a>
|
||||
|
||||
### Changing the Media Layout
|
||||
|
||||
Change how the media fits and is positioned within the card dimensions.
|
||||
|
||||
<details>
|
||||
<summary>Expand: Stretching an image</summary>
|
||||
|
||||
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
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Expand: Convert a landscape camera to a portrait live view</summary>
|
||||
|
||||
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
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Expand: Complex example to vary the media layouts for different cameras</summary>
|
||||
|
||||
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
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
<a name="card-updates"></a>
|
||||
|
||||
## Card Refreshes
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
+13
-1
@@ -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;
|
||||
|
||||
+117
-38
@@ -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
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
@@ -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,
|
||||
)}
|
||||
</div>`
|
||||
: ''}
|
||||
@@ -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,
|
||||
)}
|
||||
</div>`
|
||||
: ''}
|
||||
${this._renderOptionSetHeader('timeline')}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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`
|
||||
<img
|
||||
@load=${(e) => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
></video>
|
||||
`;
|
||||
@@ -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%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
></video>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
super.styles,
|
||||
unsafeCSS(liveHAComponentsStyle),
|
||||
css`
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+3
-1
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+4
-1
@@ -1,6 +1,9 @@
|
||||
@use 'media-layout.scss';
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: scale-down;
|
||||
|
||||
@include media-layout.media-layout();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@use 'media-layout.scss';
|
||||
|
||||
img, video {
|
||||
@include media-layout.media-layout();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@include media-layout.media-layout();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
#webrtc #video {
|
||||
@include media-layout.media-layout();
|
||||
}
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
@@ -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. <frigate-card-ha-hls-player> has similar added directly in
|
||||
// its element.
|
||||
object-fit: contain;
|
||||
}
|
||||
@include media-layout.media-layout();
|
||||
}
|
||||
|
||||
+58
-30
@@ -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<typeof pictureElementSchema>;
|
||||
const pictureElementsSchema = pictureElementSchema.array().optional();
|
||||
export type PictureElements = z.infer<typeof pictureElementsSchema>;
|
||||
|
||||
/**
|
||||
* 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<typeof mediaLayoutConfigSchema>;
|
||||
|
||||
/**
|
||||
* 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<typeof overrideConfigurationSchema>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user