Merge pull request #513 from dermotduffy/thumbnails-in-gallery
Add gallery support for showing thumbnail details
This commit is contained in:
@@ -170,7 +170,7 @@ menu:
|
|||||||
| Option | Default | Overridable | Description |
|
| Option | Default | Overridable | Description |
|
||||||
| - | - | - | - |
|
| - | - | - | - |
|
||||||
| `mode` | `hidden-top` | :white_check_mark: | The menu mode to show by default. See [menu modes](#menu-modes) below.|
|
| `mode` | `hidden-top` | :white_check_mark: | The menu mode to show by default. See [menu modes](#menu-modes) below.|
|
||||||
| `button_size` | `40px` | :white_check_mark: | The size of the menu buttons [in CSS Units](https://www.w3schools.com/cssref/css_units.asp).|
|
| `button_size` | 40 | :white_check_mark: | The size of the menu buttons in pixels. Must be >= `20`.|
|
||||||
| `buttons` | | :white_check_mark: | Whether to show or hide built-in buttons. See below. |
|
| `buttons` | | :white_check_mark: | Whether to show or hide built-in buttons. See below. |
|
||||||
|
|
||||||
#### Menu Options: Buttons
|
#### Menu Options: Buttons
|
||||||
@@ -258,7 +258,7 @@ live:
|
|||||||
| Option | Default | Overridable | Description |
|
| Option | Default | Overridable | Description |
|
||||||
| - | - | - | - |
|
| - | - | - | - |
|
||||||
| `mode` | `none` | :white_check_mark: | 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`).|
|
| `mode` | `none` | :white_check_mark: | 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` | `100px` | :white_check_mark: | The size of the thumbnails in the thumbnail carousel [in CSS Units](https://www.w3schools.com/cssref/css_units.asp).|
|
| `size` | 100 | :white_check_mark: | The size of the thumbnails in the thumbnail carousel in pixels. Must be >= `75` and <= `175`. |
|
||||||
| `show_details` | `false` | :white_check_mark: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
|
| `show_details` | `false` | :white_check_mark: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
|
||||||
| `show_controls` | `true` | :white_check_mark: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.|
|
| `show_controls` | `true` | :white_check_mark: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.|
|
||||||
| `media` | `clips` | :white_check_mark: | Whether to show `clips` or `snapshots` in the thumbnail carousel in the `live` view.|
|
| `media` | `clips` | :white_check_mark: | Whether to show `clips` or `snapshots` in the thumbnail carousel in the `live` view.|
|
||||||
@@ -276,7 +276,7 @@ live:
|
|||||||
| Option | Default | Overridable | Description |
|
| Option | Default | Overridable | Description |
|
||||||
| - | - | - | - |
|
| - | - | - | - |
|
||||||
| `style` | `chevrons` | :white_check_mark: | When viewing live cameras, what kind of controls to show to move to the previous/next camera. Acceptable values: `chevrons`, `icons`, `none` . |
|
| `style` | `chevrons` | :white_check_mark: | When viewing live cameras, what kind of controls to show to move to the previous/next camera. Acceptable values: `chevrons`, `icons`, `none` . |
|
||||||
| `size` | `48px` | :white_check_mark: | The size of the next/previous controls [in CSS Units](https://www.w3schools.com/cssref/css_units.asp).|
|
| `size` | 48 | :white_check_mark: | The size of the next/previous controls in pixels. Must be >= `20`. |
|
||||||
|
|
||||||
<a name="live-controls-title"></a>
|
<a name="live-controls-title"></a>
|
||||||
|
|
||||||
@@ -328,7 +328,7 @@ event_viewer:
|
|||||||
| Option | Default | Overridable | Description |
|
| Option | Default | Overridable | Description |
|
||||||
| - | - | - | - |
|
| - | - | - | - |
|
||||||
| `style` | `thumbnails` | :heavy_multiplication_x: | When viewing media, what kind of controls to show to move to the previous/next media item. Acceptable values: `thumbnails`, `chevrons`, `none` . |
|
| `style` | `thumbnails` | :heavy_multiplication_x: | When viewing media, what kind of controls to show to move to the previous/next media item. Acceptable values: `thumbnails`, `chevrons`, `none` . |
|
||||||
| `size` | `48px` | :heavy_multiplication_x: | The size of the next/previous controls [in CSS Units](https://www.w3schools.com/cssref/css_units.asp).|
|
| `size` | 48 | :heavy_multiplication_x: | The size of the next/previous controls in pixels. Must be >= `20`.|
|
||||||
|
|
||||||
#### Event Viewer Controls: Thumbnails
|
#### Event Viewer Controls: Thumbnails
|
||||||
|
|
||||||
@@ -343,7 +343,7 @@ event_viewer:
|
|||||||
| Option | Default | Overridable | Description |
|
| Option | Default | Overridable | Description |
|
||||||
| - | - | - | - |
|
| - | - | - | - |
|
||||||
| `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`).|
|
| `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` | `100px` | :heavy_multiplication_x: | The size of the thumbnails in the thumbnail carousel [in CSS Units](https://www.w3schools.com/cssref/css_units.asp).|
|
| `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_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
|
||||||
| `show_controls` | `true` | :heavy_multiplication_x: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.|
|
| `show_controls` | `true` | :heavy_multiplication_x: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.|
|
||||||
|
|
||||||
@@ -374,7 +374,9 @@ event_gallery:
|
|||||||
|
|
||||||
| Option | Default | Overridable | Description |
|
| Option | Default | Overridable | Description |
|
||||||
| - | - | - | - |
|
| - | - | - | - |
|
||||||
| `min_columns` | `5` | :heavy_multiplication_x: | The minimum number of columns to show in the gallery -- smaller values will render fewer but larger thumbnail columns. Thumbnails will never be stretched beyond their intrinsic size (typically `175px`). All available space (in normal mode, or fullscreen mode) will be occupied by the gallery, so more columns than `min_columns` will often be rendered if space allows for more full-sized thumbnails. For normal sized Lovelace cards (`492px` wide), this typically means there'll never be fewer than 3 columns rendered (as otherwise the thumbnails would need to stretch beyond their actual size). Must be `1 <= x <= 10`. |
|
| `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_controls` | `true` | :heavy_multiplication_x: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.|
|
||||||
| `actions` | | :heavy_multiplication_x: | Actions to use for all views that use the `event_gallery` (e.g. `clips`, `snapshots`). See [actions](#actions) below.|
|
| `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
|
### Image Options
|
||||||
@@ -423,7 +425,7 @@ timeline:
|
|||||||
| Option | Default | Overridable | Description |
|
| Option | Default | Overridable | Description |
|
||||||
| - | - | - | - |
|
| - | - | - | - |
|
||||||
| `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`).|
|
| `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` | `100px` | :heavy_multiplication_x: | The size of the thumbnails in the thumbnail carousel [in CSS Units](https://www.w3schools.com/cssref/css_units.asp).|
|
| `size` | 100 | :heavy_multiplication_x: | The size of the thumbnails in the thumbnail carousel 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_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
|
||||||
| `show_controls` | `true` | :heavy_multiplication_x: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.|
|
| `show_controls` | `true` | :heavy_multiplication_x: | Whether to show event controls (e.g. timeline icon, favorite icon) alongside the thumbnail.|
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -18,7 +18,6 @@
|
|||||||
"@cycjimmy/jsmpeg-player": "^5.1.1",
|
"@cycjimmy/jsmpeg-player": "^5.1.1",
|
||||||
"@egjs/hammerjs": "^2.0.17",
|
"@egjs/hammerjs": "^2.0.17",
|
||||||
"@lit-labs/task": "^1.1.1",
|
"@lit-labs/task": "^1.1.1",
|
||||||
"@material/image-list": "^13.0.0",
|
|
||||||
"@material/mwc-menu": "^0.25.3",
|
"@material/mwc-menu": "^0.25.3",
|
||||||
"@material/rtl": "^13.0.0",
|
"@material/rtl": "^13.0.0",
|
||||||
"@types/bluebird": "^3.5.36",
|
"@types/bluebird": "^3.5.36",
|
||||||
@@ -32,7 +31,7 @@
|
|||||||
"keycharm": "^0.4.0",
|
"keycharm": "^0.4.0",
|
||||||
"lit": "^2.2.1",
|
"lit": "^2.2.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.2",
|
||||||
"propagating-hammerjs": "^2.0.1",
|
"propagating-hammerjs": "^2.0.1",
|
||||||
"quick-lru": "^6.1.0",
|
"quick-lru": "^6.1.0",
|
||||||
"screenfull": "^6.0.1",
|
"screenfull": "^6.0.1",
|
||||||
|
|||||||
+106
-95
@@ -1,22 +1,30 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
import {
|
||||||
|
CSSResultGroup,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
|
unsafeCSS,
|
||||||
|
} from 'lit';
|
||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
GalleryConfig,
|
GalleryConfig,
|
||||||
THUMBNAIL_WIDTH_MAX,
|
|
||||||
frigateCardConfigDefaults,
|
frigateCardConfigDefaults,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { BrowseMediaUtil } from '../browse-media-util.js';
|
import { BrowseMediaUtil } from '../browse-media-util.js';
|
||||||
import { View } from '../view.js';
|
import { View } from '../view.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
||||||
import { renderProgressIndicator } from './message.js';
|
import { renderProgressIndicator } from './message.js';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../common.js';
|
import { stopEventFromActivatingCardWideActions } from '../common.js';
|
||||||
|
|
||||||
|
import './thumbnail.js';
|
||||||
|
|
||||||
import galleryStyle from '../scss/gallery.scss';
|
import galleryStyle from '../scss/gallery.scss';
|
||||||
|
|
||||||
@customElement('frigate-card-gallery')
|
@customElement('frigate-card-gallery')
|
||||||
@@ -77,7 +85,13 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
* Get element styles.
|
* Get element styles.
|
||||||
*/
|
*/
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(galleryStyle);
|
return css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,9 +108,6 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
|
|
||||||
protected _resizeObserver: ResizeObserver;
|
protected _resizeObserver: ResizeObserver;
|
||||||
|
|
||||||
@state()
|
|
||||||
protected _columns = frigateCardConfigDefaults.event_gallery.min_columns;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this._resizeObserver = new ResizeObserver(this._resizeHandler.bind(this));
|
this._resizeObserver = new ResizeObserver(this._resizeHandler.bind(this));
|
||||||
@@ -122,10 +133,16 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
* Handle gallery resize.
|
* Handle gallery resize.
|
||||||
*/
|
*/
|
||||||
protected _resizeHandler(): void {
|
protected _resizeHandler(): void {
|
||||||
this._columns = Math.max(
|
const thumbnailSize =
|
||||||
this.galleryConfig?.min_columns ??
|
this.galleryConfig?.controls.thumbnails.size ??
|
||||||
frigateCardConfigDefaults.event_gallery.min_columns,
|
frigateCardConfigDefaults.event_gallery.controls.thumbnails.size;
|
||||||
Math.ceil(this.clientWidth / THUMBNAIL_WIDTH_MAX),
|
this.style.setProperty(
|
||||||
|
'--frigate-card-gallery-columns',
|
||||||
|
String(
|
||||||
|
!this.galleryConfig?.controls.thumbnails.show_details
|
||||||
|
? Math.round(this.clientWidth / thumbnailSize)
|
||||||
|
: Math.max(1, Math.floor(this.clientWidth / THUMBNAIL_DETAILS_WIDTH_MIN)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +158,26 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an update will occur.
|
||||||
|
* @param changedProps The changed properties
|
||||||
|
*/
|
||||||
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
|
if (changedProps.has('galleryConfig')) {
|
||||||
|
if (this.galleryConfig?.controls.thumbnails.show_details) {
|
||||||
|
this.setAttribute('details', '');
|
||||||
|
} else {
|
||||||
|
this.removeAttribute('details');
|
||||||
|
}
|
||||||
|
if (this.galleryConfig?.controls.thumbnails.size) {
|
||||||
|
this.style.setProperty(
|
||||||
|
'--frigate-card-thumbnail-size',
|
||||||
|
`${this.galleryConfig.controls.thumbnails.size}px`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master render method.
|
* Master render method.
|
||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
@@ -156,93 +193,67 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemStyle = {
|
return html`
|
||||||
// Controls the number of columns in the gallery (allows for 5px gutter).
|
|
||||||
width: `calc(${100 / this._columns}% - 5.25px)`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const folderStyle = {
|
|
||||||
// Values derived from experimentation on typical Lovelace card sizes.
|
|
||||||
'font-size': `${Math.min(
|
|
||||||
1.1,
|
|
||||||
(0.6 * (this.clientWidth / this._columns)) / 50.0,
|
|
||||||
)}em`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return html` <ul class="mdc-image-list frigate-card-gallery">
|
|
||||||
${this._showBackArrow()
|
${this._showBackArrow()
|
||||||
? html`<li class="mdc-image-list__item" style="${styleMap(itemStyle)}">
|
? html` <ha-card
|
||||||
<div class="mdc-image-list__image-aspect-container">
|
@click=${(ev) => {
|
||||||
<div class="mdc-image-list__image">
|
if (this.view && this.view.previous) {
|
||||||
<ha-card
|
this.view.previous.dispatchChangeEvent(this);
|
||||||
@click=${(ev) => {
|
}
|
||||||
if (this.view && this.view.previous) {
|
stopEventFromActivatingCardWideActions(ev);
|
||||||
this.view.previous.dispatchChangeEvent(this);
|
}}
|
||||||
}
|
outlined=""
|
||||||
stopEventFromActivatingCardWideActions(ev);
|
>
|
||||||
}}
|
<ha-icon .icon=${'mdi:arrow-left'}></ha-icon>
|
||||||
outlined=""
|
</ha-card>`
|
||||||
class="frigate-card-gallery-folder"
|
|
||||||
>
|
|
||||||
<ha-icon .icon=${'mdi:arrow-left'}></ha-icon>
|
|
||||||
</ha-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>`
|
|
||||||
: ''}
|
: ''}
|
||||||
${this.view.target.children.map(
|
${this.view.target.children.map(
|
||||||
(child, index) =>
|
(child, index) =>
|
||||||
html` <li class="mdc-image-list__item" style="${styleMap(itemStyle)}">
|
html`
|
||||||
<div class="mdc-image-list__image-aspect-container">
|
${child.can_expand
|
||||||
${child.can_expand
|
? html`
|
||||||
? html`<div class="mdc-image-list__image">
|
<ha-card
|
||||||
<ha-card
|
@click=${(ev) => {
|
||||||
@click=${(ev) => {
|
if (this.hass && this.view) {
|
||||||
if (this.hass && this.view) {
|
BrowseMediaUtil.fetchChildMediaAndDispatchViewChange(
|
||||||
BrowseMediaUtil.fetchChildMediaAndDispatchViewChange(
|
this,
|
||||||
this,
|
this.hass,
|
||||||
this.hass,
|
this.view,
|
||||||
this.view,
|
child,
|
||||||
child,
|
);
|
||||||
);
|
}
|
||||||
}
|
stopEventFromActivatingCardWideActions(ev);
|
||||||
stopEventFromActivatingCardWideActions(ev);
|
}}
|
||||||
}}
|
outlined=""
|
||||||
outlined=""
|
class="foo"
|
||||||
class="frigate-card-gallery-folder"
|
>
|
||||||
>
|
<div>${child.title}</div>
|
||||||
<div style="${styleMap(folderStyle)}">${child.title}</div>
|
</ha-card>
|
||||||
</ha-card>
|
`
|
||||||
</div>`
|
: child.thumbnail
|
||||||
: child.thumbnail
|
? html`<frigate-card-thumbnail
|
||||||
? html`<img
|
.view=${this.view}
|
||||||
aria-label="${child.title}"
|
.target=${this.view?.target ?? null}
|
||||||
class="mdc-image-list__image"
|
.childIndex=${index}
|
||||||
src="${child.thumbnail}"
|
?details=${!!this.galleryConfig?.controls.thumbnails.show_details}
|
||||||
title="${child.title}"
|
?controls=${!!this.galleryConfig?.controls.thumbnails.show_controls}
|
||||||
@click=${(ev: Event) => {
|
@click=${(ev: Event) => {
|
||||||
if (this.view) {
|
if (this.view) {
|
||||||
this.view
|
this.view
|
||||||
.evolve({
|
.evolve({
|
||||||
view: this.view.is('clips') ? 'clip' : 'snapshot',
|
view: this.view.is('clips') ? 'clip' : 'snapshot',
|
||||||
childIndex: index,
|
childIndex: index,
|
||||||
})
|
})
|
||||||
.dispatchChangeEvent(this);
|
.dispatchChangeEvent(this);
|
||||||
}
|
}
|
||||||
stopEventFromActivatingCardWideActions(ev);
|
stopEventFromActivatingCardWideActions(ev);
|
||||||
}}
|
}}
|
||||||
/>${child.frigate?.event?.retain_indefinitely
|
>
|
||||||
? html`<ha-icon
|
</frigate-card-thumbnail>`
|
||||||
class="favorite"
|
: ``}
|
||||||
icon="mdi:star"
|
`,
|
||||||
title=${localize('thumbnail.retain_indefinitely')}
|
|
||||||
/>`
|
|
||||||
: ``}`
|
|
||||||
: ``}
|
|
||||||
</div>
|
|
||||||
</li>`,
|
|
||||||
)}
|
)}
|
||||||
</ul>`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -60,7 +60,10 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
set menuConfig(menuConfig: MenuConfig) {
|
set menuConfig(menuConfig: MenuConfig) {
|
||||||
this._menuConfig = menuConfig;
|
this._menuConfig = menuConfig;
|
||||||
if (menuConfig) {
|
if (menuConfig) {
|
||||||
this.style.setProperty('--frigate-card-menu-button-size', menuConfig.button_size);
|
this.style.setProperty(
|
||||||
|
'--frigate-card-menu-button-size',
|
||||||
|
`${menuConfig.button_size}px`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Store the menu mode as an attribute (used for CSS attribute selectors).
|
// Store the menu mode as an attribute (used for CSS attribute selectors).
|
||||||
this.setAttribute('data-mode', menuConfig.mode);
|
this.setAttribute('data-mode', menuConfig.mode);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
|
|
||||||
set controlConfig(controlConfig: NextPreviousControlConfig | undefined) {
|
set controlConfig(controlConfig: NextPreviousControlConfig | undefined) {
|
||||||
if (controlConfig?.size) {
|
if (controlConfig?.size) {
|
||||||
this.style.setProperty('--frigate-card-next-prev-size', controlConfig.size);
|
this.style.setProperty('--frigate-card-next-prev-size', `${controlConfig.size}px`);
|
||||||
}
|
}
|
||||||
this._controlConfig = controlConfig;
|
this._controlConfig = controlConfig;
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
public disabled = false;
|
public disabled = false;
|
||||||
|
|
||||||
// Label that is used for ARIA support and as tooltip.
|
// Label that is used for ARIA support and as tooltip.
|
||||||
@property() label = "";
|
@property() label = '';
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (this.disabled || !this._controlConfig || this._controlConfig.style == 'none') {
|
if (this.disabled || !this._controlConfig || this._controlConfig.style == 'none') {
|
||||||
@@ -50,18 +50,15 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
|||||||
if (['chevrons', 'icons'].includes(this._controlConfig.style)) {
|
if (['chevrons', 'icons'].includes(this._controlConfig.style)) {
|
||||||
let icon: string;
|
let icon: string;
|
||||||
if (this._controlConfig.style === 'chevrons') {
|
if (this._controlConfig.style === 'chevrons') {
|
||||||
icon = this.direction == 'previous' ? 'mdi:chevron-left' : 'mdi:chevron-right';
|
icon = this.direction == 'previous' ? 'mdi:chevron-left' : 'mdi:chevron-right';
|
||||||
} else {
|
} else {
|
||||||
if (!this.icon) {
|
if (!this.icon) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
icon = this.icon
|
icon = this.icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html` <ha-icon-button
|
return html` <ha-icon-button class="${classMap(classes)}" .label=${this.label}>
|
||||||
class="${classMap(classes)}"
|
|
||||||
.label=${this.label}
|
|
||||||
>
|
|
||||||
<ha-icon icon=${icon}></ha-icon>
|
<ha-icon icon=${icon}></ha-icon>
|
||||||
</ha-icon-button>`;
|
</ha-icon-button>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { EmblaOptionsType } from 'embla-carousel';
|
|||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { isEqual } from 'lodash-es';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
FrigateBrowseMediaSource,
|
FrigateBrowseMediaSource,
|
||||||
|
|||||||
+45
-24
@@ -12,6 +12,44 @@ import {
|
|||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
|
|
||||||
import thumbnailStyle from '../scss/thumbnail.scss';
|
import thumbnailStyle from '../scss/thumbnail.scss';
|
||||||
|
import thumbnailDetailsStyle from '../scss/thumbnail-details.scss';
|
||||||
|
|
||||||
|
// The minimum width of a thumbnail with details enabled.
|
||||||
|
export const THUMBNAIL_DETAILS_WIDTH_MIN = 300;
|
||||||
|
|
||||||
|
@customElement('frigate-card-thumbnail-details')
|
||||||
|
export class FrigateCardThumbnailDetails extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public event?: FrigateEvent;
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this.event) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const score = (this.event.top_score * 100).toFixed(2) + '%';
|
||||||
|
return html`<div class="left">
|
||||||
|
<div class="larger">${prettifyFrigateName(this.event.label)}</div>
|
||||||
|
<div>
|
||||||
|
<span class="heading">${localize('event.start')}:</span>
|
||||||
|
<span>${format(fromUnixTime(this.event.start_time), 'HH:mm:ss')}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="heading">${localize('event.duration')}:</span>
|
||||||
|
<span>${getEventDurationString(this.event)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<span class="larger">${score}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get element styles.
|
||||||
|
*/
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return unsafeCSS(thumbnailDetailsStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@customElement('frigate-card-thumbnail')
|
@customElement('frigate-card-thumbnail')
|
||||||
export class FrigateCardThumbnail extends LitElement {
|
export class FrigateCardThumbnail extends LitElement {
|
||||||
@@ -21,9 +59,9 @@ export class FrigateCardThumbnail extends LitElement {
|
|||||||
@property({ attribute: true, type: Boolean })
|
@property({ attribute: true, type: Boolean })
|
||||||
public controls = false;
|
public controls = false;
|
||||||
|
|
||||||
@property({ attribute: true })
|
@property({ attribute: true, type: Number })
|
||||||
set thumbnail_size(size: string) {
|
set thumbnail_size(size: number) {
|
||||||
this.style.setProperty('--frigate-card-thumbnail-size', String(size));
|
this.style.setProperty('--frigate-card-thumbnail-size', `${size}px`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
@@ -33,7 +71,7 @@ export class FrigateCardThumbnail extends LitElement {
|
|||||||
protected view?: Readonly<View>;
|
protected view?: Readonly<View>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public target?: FrigateBrowseMediaSource;
|
public target?: FrigateBrowseMediaSource | null;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public childIndex?: number;
|
public childIndex?: number;
|
||||||
@@ -91,26 +129,9 @@ export class FrigateCardThumbnail extends LitElement {
|
|||||||
/>`
|
/>`
|
||||||
: ``}
|
: ``}
|
||||||
${this.details && event
|
${this.details && event
|
||||||
? html` <div class="details">
|
? html`<frigate-card-thumbnail-details
|
||||||
<div class="left">
|
.event=${event}
|
||||||
<div class="larger">${prettifyFrigateName(event.label)}</div>
|
></frigate-card-thumbnail-details>`
|
||||||
<div>
|
|
||||||
<span>
|
|
||||||
<span class="heading">${localize('event.start')}:</span>
|
|
||||||
${format(fromUnixTime(event.start_time), 'HH:mm:ss')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>
|
|
||||||
<span class="heading">${localize('event.duration')}:</span>
|
|
||||||
${getEventDurationString(event)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="right">
|
|
||||||
<div class="larger">${(event.top_score * 100).toFixed(2) + '%'}</div>
|
|
||||||
</div>
|
|
||||||
</div>`
|
|
||||||
: html``}
|
: html``}
|
||||||
${this.controls
|
${this.controls
|
||||||
? html`<ha-icon
|
? html`<ha-icon
|
||||||
|
|||||||
+118
-37
@@ -1,4 +1,4 @@
|
|||||||
import { get, set } from 'lodash-es';
|
import { get, isEqual, set } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
CONF_CAMERAS,
|
CONF_CAMERAS,
|
||||||
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
|
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
|
||||||
@@ -11,7 +11,10 @@ import {
|
|||||||
CONF_EVENT_VIEWER_AUTO_PLAY,
|
CONF_EVENT_VIEWER_AUTO_PLAY,
|
||||||
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
|
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
|
||||||
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
|
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
|
||||||
|
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE,
|
||||||
CONF_IMAGE_URL,
|
CONF_IMAGE_URL,
|
||||||
|
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
|
||||||
|
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
|
||||||
CONF_LIVE_PRELOAD,
|
CONF_LIVE_PRELOAD,
|
||||||
CONF_LIVE_WEBRTC_CARD,
|
CONF_LIVE_WEBRTC_CARD,
|
||||||
CONF_MENU,
|
CONF_MENU,
|
||||||
@@ -22,7 +25,13 @@ import {
|
|||||||
CONF_VIEW_TIMEOUT_SECONDS,
|
CONF_VIEW_TIMEOUT_SECONDS,
|
||||||
CONF_VIEW_UPDATE_ENTITIES,
|
CONF_VIEW_UPDATE_ENTITIES,
|
||||||
} from './const';
|
} from './const';
|
||||||
import { RawFrigateCardConfig, RawFrigateCardConfigArray } from './types';
|
import {
|
||||||
|
BUTTON_SIZE_MIN,
|
||||||
|
RawFrigateCardConfig,
|
||||||
|
RawFrigateCardConfigArray,
|
||||||
|
THUMBNAIL_WIDTH_MAX,
|
||||||
|
THUMBNAIL_WIDTH_MIN,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a configuration value.
|
* Set a configuration value.
|
||||||
@@ -131,7 +140,7 @@ export const copyConfig = function (obj: RawFrigateCardConfig): RawFrigateCardCo
|
|||||||
* @param value The value.
|
* @param value The value.
|
||||||
* @returns `true` is the value is not an object.
|
* @returns `true` is the value is not an object.
|
||||||
*/
|
*/
|
||||||
const isNotObject = function (value: unknown) {
|
const isNotObject = function (value: unknown): unknown | undefined {
|
||||||
return typeof value !== 'object' ? value : undefined;
|
return typeof value !== 'object' ? value : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -140,10 +149,62 @@ const isNotObject = function (value: unknown) {
|
|||||||
* @param value The value.
|
* @param value The value.
|
||||||
* @returns A number or undefined.
|
* @returns A number or undefined.
|
||||||
*/
|
*/
|
||||||
const toNumberOrIgnore = function (value: unknown) {
|
const toNumberOrIgnore = function (value: unknown): number | undefined {
|
||||||
return isNaN(value as number) ? undefined : Number(value);
|
return isNaN(value as number) ? undefined : Number(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a transform that will cap a numeric value.
|
||||||
|
* @param value The value.
|
||||||
|
* @returns A number or null.
|
||||||
|
*/
|
||||||
|
const createRangedTransform = function (
|
||||||
|
transform: (value: unknown) => unknown,
|
||||||
|
min?: number,
|
||||||
|
max?: number,
|
||||||
|
): (valueIn: unknown) => unknown {
|
||||||
|
return (value: unknown): unknown => {
|
||||||
|
let transformed = transform(value);
|
||||||
|
if (typeof transformed !== 'number') {
|
||||||
|
return transformed;
|
||||||
|
}
|
||||||
|
transformed = min ? Math.max(min, transformed as number) : transformed;
|
||||||
|
transformed = max ? Math.min(max, transformed as number) : transformed;
|
||||||
|
return transformed;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a value from 'XXpx' to XX (as a number).
|
||||||
|
* @param value Incoming value.
|
||||||
|
* @returns A number, null if the property should be deleted or undefined if it
|
||||||
|
* should be ignored.
|
||||||
|
*/
|
||||||
|
const toPixelsOrDelete = function (value: unknown): number | null | undefined {
|
||||||
|
// Ignore the value if it's a number.
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
// Delete the value if it's not a string.
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Remove 'px' and return the number, unless it's an invalid number -- then
|
||||||
|
// delete it.
|
||||||
|
value = value.replace(/px$/i, '');
|
||||||
|
return isNaN(value as number) ? null : Number(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a property be deleted.
|
||||||
|
* @param _value Inbound value (not required).
|
||||||
|
* @returns `null` to request the property be deleted.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const deleteProperty = function (_value: unknown): number | null | undefined {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move a property from one location to another.
|
* Move a property from one location to another.
|
||||||
* @param obj The configuration object in which the property resides.
|
* @param obj The configuration object in which the property resides.
|
||||||
@@ -158,13 +219,21 @@ export const moveConfigValue = (
|
|||||||
newPath: string,
|
newPath: string,
|
||||||
transform?: (valueIn: unknown) => unknown,
|
transform?: (valueIn: unknown) => unknown,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
let value = getConfigValue(obj, oldPath);
|
const inValue = getConfigValue(obj, oldPath);
|
||||||
if (transform) {
|
if (inValue === undefined) {
|
||||||
value = transform(value);
|
return false;
|
||||||
}
|
}
|
||||||
if (typeof value !== 'undefined') {
|
const outValue = transform ? transform(inValue) : inValue;
|
||||||
|
if (oldPath === newPath && isEqual(inValue, outValue)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (outValue === null) {
|
||||||
deleteConfigValue(obj, oldPath);
|
deleteConfigValue(obj, oldPath);
|
||||||
setConfigValue(obj, newPath, value);
|
return true;
|
||||||
|
}
|
||||||
|
if (outValue !== undefined) {
|
||||||
|
deleteConfigValue(obj, oldPath);
|
||||||
|
setConfigValue(obj, newPath, outValue);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -197,33 +266,6 @@ const upgradeMoveTo = function (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Upgrade a property by changing it if it is present.
|
|
||||||
* @param path The property path.
|
|
||||||
* @param transform A callback that transforms the old value to the new value,
|
|
||||||
* if undefined is returned the property is removed.
|
|
||||||
* @returns `true` if the configuration was modified.
|
|
||||||
*/
|
|
||||||
const upgradeChangeIfPresent = function (
|
|
||||||
path: string,
|
|
||||||
transform: (valueIn: unknown) => unknown,
|
|
||||||
): (obj: RawFrigateCardConfig) => boolean {
|
|
||||||
return function (obj: RawFrigateCardConfig): boolean {
|
|
||||||
const oldValue = getConfigValue(obj, path);
|
|
||||||
if (oldValue !== undefined) {
|
|
||||||
const newValue = transform(oldValue);
|
|
||||||
if (newValue === undefined) {
|
|
||||||
deleteConfigValue(obj, path);
|
|
||||||
return true;
|
|
||||||
} else if (newValue !== oldValue) {
|
|
||||||
setConfigValue(obj, path, newValue);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upgrade by moving a property from one location to another, and moving a
|
* Upgrade by moving a property from one location to another, and moving a
|
||||||
* property specified in a top-level overrides object.
|
* property specified in a top-level overrides object.
|
||||||
@@ -249,6 +291,19 @@ const upgradeMoveToWithOverrides = function (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrade a property in place with overrides.
|
||||||
|
* @param path The old property path.
|
||||||
|
* @param transform An optional transform for the value.
|
||||||
|
* @returns A function that returns `true` if the configuration was modified.
|
||||||
|
*/
|
||||||
|
const upgradeWithOverrides = function (
|
||||||
|
path: string,
|
||||||
|
transform?: (valueIn: unknown) => unknown,
|
||||||
|
): (obj: RawFrigateCardConfig) => boolean {
|
||||||
|
return upgradeMoveToWithOverrides(path, path, transform);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a path to an array, apply an upgrade to each object in the array.
|
* Given a path to an array, apply an upgrade to each object in the array.
|
||||||
* @param arrayPath The path to the array to upgrade.
|
* @param arrayPath The path to the array to upgrade.
|
||||||
@@ -377,11 +432,37 @@ const UPGRADES = [
|
|||||||
// v3.0.0-rc.1 -> v3.0.0-rc.2
|
// v3.0.0-rc.1 -> v3.0.0-rc.2
|
||||||
upgradeArrayValue(
|
upgradeArrayValue(
|
||||||
CONF_CAMERAS,
|
CONF_CAMERAS,
|
||||||
upgradeChangeIfPresent('live_provider', (val) =>
|
upgradeWithOverrides('live_provider', (val) =>
|
||||||
val === 'frigate' ? 'ha' : val === 'webrtc' ? 'webrtc-card' : val,
|
val === 'frigate' ? 'ha' : val === 'webrtc' ? 'webrtc-card' : val,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
upgradeArrayValue(CONF_CAMERAS, upgradeMoveTo('webrtc', 'webrtc_card')),
|
upgradeArrayValue(CONF_CAMERAS, upgradeMoveTo('webrtc', 'webrtc_card')),
|
||||||
upgradeMoveToWithOverrides('live.webrtc', CONF_LIVE_WEBRTC_CARD),
|
upgradeMoveToWithOverrides('live.webrtc', CONF_LIVE_WEBRTC_CARD),
|
||||||
upgradeMoveToWithOverrides('image.src', CONF_IMAGE_URL),
|
upgradeMoveToWithOverrides('image.src', CONF_IMAGE_URL),
|
||||||
|
|
||||||
|
// v3.0.0 -> v4.0.0-rc.1
|
||||||
|
upgradeWithOverrides(
|
||||||
|
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
|
||||||
|
createRangedTransform(toPixelsOrDelete, THUMBNAIL_WIDTH_MIN, THUMBNAIL_WIDTH_MAX),
|
||||||
|
),
|
||||||
|
upgradeWithOverrides(
|
||||||
|
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE,
|
||||||
|
createRangedTransform(toPixelsOrDelete, THUMBNAIL_WIDTH_MIN, THUMBNAIL_WIDTH_MAX),
|
||||||
|
),
|
||||||
|
upgradeWithOverrides(
|
||||||
|
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
|
||||||
|
createRangedTransform(toPixelsOrDelete, BUTTON_SIZE_MIN),
|
||||||
|
),
|
||||||
|
upgradeWithOverrides(
|
||||||
|
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
|
||||||
|
createRangedTransform(toPixelsOrDelete, BUTTON_SIZE_MIN),
|
||||||
|
),
|
||||||
|
upgradeWithOverrides(
|
||||||
|
CONF_MENU_BUTTON_SIZE,
|
||||||
|
createRangedTransform(toPixelsOrDelete, BUTTON_SIZE_MIN),
|
||||||
|
),
|
||||||
|
upgradeWithOverrides(
|
||||||
|
'event_gallery.min_columns',
|
||||||
|
deleteProperty
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
+6
-2
@@ -29,8 +29,12 @@ export const CONF_VIEW_UPDATE_ENTITIES = `${CONF_VIEW}.update_entities` as const
|
|||||||
export const CONF_VIEW_UPDATE_SECONDS = `${CONF_VIEW}.update_seconds` as const;
|
export const CONF_VIEW_UPDATE_SECONDS = `${CONF_VIEW}.update_seconds` as const;
|
||||||
|
|
||||||
export const CONF_EVENT_GALLERY = 'event_gallery' as const;
|
export const CONF_EVENT_GALLERY = 'event_gallery' as const;
|
||||||
export const CONF_EVENT_GALLERY_MIN_COLUMNS =
|
export const CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS =
|
||||||
`${CONF_EVENT_GALLERY}.min_columns` as const;
|
`${CONF_EVENT_GALLERY}.controls.thumbnails.show_details` as const;
|
||||||
|
export const CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_CONTROLS =
|
||||||
|
`${CONF_EVENT_GALLERY}.controls.thumbnails.show_controls` as const;
|
||||||
|
export const CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SIZE =
|
||||||
|
`${CONF_EVENT_GALLERY}.controls.thumbnails.size` as const;
|
||||||
|
|
||||||
export const CONF_EVENT_VIEWER = 'event_viewer' as const;
|
export const CONF_EVENT_VIEWER = 'event_viewer' as const;
|
||||||
export const CONF_EVENT_VIEWER_AUTO_PLAY = `${CONF_EVENT_VIEWER}.auto_play` as const;
|
export const CONF_EVENT_VIEWER_AUTO_PLAY = `${CONF_EVENT_VIEWER}.auto_play` as const;
|
||||||
|
|||||||
+48
-10
@@ -5,9 +5,12 @@ import { customElement, property, state } from 'lit/decorators.js';
|
|||||||
import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers';
|
import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers';
|
||||||
import { localize } from './localize/localize.js';
|
import { localize } from './localize/localize.js';
|
||||||
import {
|
import {
|
||||||
frigateCardConfigDefaults,
|
BUTTON_SIZE_MIN,
|
||||||
RawFrigateCardConfig,
|
RawFrigateCardConfig,
|
||||||
RawFrigateCardConfigArray,
|
RawFrigateCardConfigArray,
|
||||||
|
THUMBNAIL_WIDTH_MAX,
|
||||||
|
THUMBNAIL_WIDTH_MIN,
|
||||||
|
frigateCardConfigDefaults,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -26,7 +29,9 @@ import {
|
|||||||
CONF_CAMERAS_ARRAY_ZONE,
|
CONF_CAMERAS_ARRAY_ZONE,
|
||||||
CONF_DIMENSIONS_ASPECT_RATIO,
|
CONF_DIMENSIONS_ASPECT_RATIO,
|
||||||
CONF_DIMENSIONS_ASPECT_RATIO_MODE,
|
CONF_DIMENSIONS_ASPECT_RATIO_MODE,
|
||||||
CONF_EVENT_GALLERY_MIN_COLUMNS,
|
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_CONTROLS,
|
||||||
|
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||||
|
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SIZE,
|
||||||
CONF_EVENT_VIEWER_AUTO_PLAY,
|
CONF_EVENT_VIEWER_AUTO_PLAY,
|
||||||
CONF_EVENT_VIEWER_AUTO_UNMUTE,
|
CONF_EVENT_VIEWER_AUTO_UNMUTE,
|
||||||
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
|
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
|
||||||
@@ -843,7 +848,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
? html`
|
? html`
|
||||||
<div class="values">
|
<div class="values">
|
||||||
${this._renderOptionSelector(CONF_MENU_MODE, this._menuModes)}
|
${this._renderOptionSelector(CONF_MENU_MODE, this._menuModes)}
|
||||||
${this._renderStringInput(CONF_MENU_BUTTON_SIZE)}
|
${this._renderNumberInput(CONF_MENU_BUTTON_SIZE, BUTTON_SIZE_MIN)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_MENU_BUTTONS_FRIGATE,
|
CONF_MENU_BUTTONS_FRIGATE,
|
||||||
defaults.menu.buttons.frigate,
|
defaults.menu.buttons.frigate,
|
||||||
@@ -900,7 +905,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
|
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
|
||||||
this._liveNextPreviousControlStyles,
|
this._liveNextPreviousControlStyles,
|
||||||
)}
|
)}
|
||||||
${this._renderStringInput(CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE)}
|
${this._renderNumberInput(
|
||||||
|
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
|
||||||
|
BUTTON_SIZE_MIN,
|
||||||
|
)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
|
CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
|
||||||
this._thumbnailModes,
|
this._thumbnailModes,
|
||||||
@@ -909,7 +917,11 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA,
|
CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA,
|
||||||
this._thumbnailMedias,
|
this._thumbnailMedias,
|
||||||
)}
|
)}
|
||||||
${this._renderStringInput(CONF_LIVE_CONTROLS_THUMBNAILS_SIZE)}
|
${this._renderNumberInput(
|
||||||
|
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
|
||||||
|
THUMBNAIL_WIDTH_MIN,
|
||||||
|
THUMBNAIL_WIDTH_MAX,
|
||||||
|
)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
CONF_LIVE_CONTROLS_TITLE_MODE,
|
CONF_LIVE_CONTROLS_TITLE_MODE,
|
||||||
this._titleModes,
|
this._titleModes,
|
||||||
@@ -937,7 +949,19 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
${this._renderOptionSetHeader('event_gallery')}
|
${this._renderOptionSetHeader('event_gallery')}
|
||||||
${options.event_gallery.show
|
${options.event_gallery.show
|
||||||
? html` <div class="values">
|
? html` <div class="values">
|
||||||
${this._renderNumberInput(CONF_EVENT_GALLERY_MIN_COLUMNS, 1, 10)}
|
${this._renderNumberInput(
|
||||||
|
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SIZE,
|
||||||
|
THUMBNAIL_WIDTH_MIN,
|
||||||
|
THUMBNAIL_WIDTH_MAX,
|
||||||
|
)}
|
||||||
|
${this._renderSwitch(
|
||||||
|
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||||
|
defaults.event_viewer.controls.thumbnails.show_details,
|
||||||
|
)}
|
||||||
|
${this._renderSwitch(
|
||||||
|
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_CONTROLS,
|
||||||
|
defaults.event_viewer.controls.thumbnails.show_controls,
|
||||||
|
)}
|
||||||
</div>`
|
</div>`
|
||||||
: ''}
|
: ''}
|
||||||
${this._renderOptionSetHeader('event_viewer')}
|
${this._renderOptionSetHeader('event_viewer')}
|
||||||
@@ -963,12 +987,19 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
|
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
|
||||||
this._eventViewerNextPreviousControlStyles,
|
this._eventViewerNextPreviousControlStyles,
|
||||||
)}
|
)}
|
||||||
${this._renderStringInput(CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE)}
|
${this._renderNumberInput(
|
||||||
|
CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
|
||||||
|
BUTTON_SIZE_MIN,
|
||||||
|
)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE,
|
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE,
|
||||||
this._thumbnailModes,
|
this._thumbnailModes,
|
||||||
)}
|
)}
|
||||||
${this._renderStringInput(CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE)}
|
${this._renderNumberInput(
|
||||||
|
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SIZE,
|
||||||
|
THUMBNAIL_WIDTH_MIN,
|
||||||
|
THUMBNAIL_WIDTH_MAX,
|
||||||
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||||
defaults.event_viewer.controls.thumbnails.show_details,
|
defaults.event_viewer.controls.thumbnails.show_details,
|
||||||
@@ -1005,12 +1036,19 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
? html` <div class="values">
|
? html` <div class="values">
|
||||||
${this._renderNumberInput(CONF_TIMELINE_WINDOW_SECONDS)}
|
${this._renderNumberInput(CONF_TIMELINE_WINDOW_SECONDS)}
|
||||||
${this._renderNumberInput(CONF_TIMELINE_CLUSTERING_THRESHOLD)}
|
${this._renderNumberInput(CONF_TIMELINE_CLUSTERING_THRESHOLD)}
|
||||||
${this._renderOptionSelector(CONF_TIMELINE_MEDIA, this._timelineMediaTypes)}
|
${this._renderOptionSelector(
|
||||||
|
CONF_TIMELINE_MEDIA,
|
||||||
|
this._timelineMediaTypes,
|
||||||
|
)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
|
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
|
||||||
this._thumbnailModes,
|
this._thumbnailModes,
|
||||||
)}
|
)}
|
||||||
${this._renderStringInput(CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE)}
|
${this._renderNumberInput(
|
||||||
|
CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE,
|
||||||
|
THUMBNAIL_WIDTH_MIN,
|
||||||
|
THUMBNAIL_WIDTH_MAX,
|
||||||
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||||
defaults.timeline.controls.thumbnails.show_details,
|
defaults.timeline.controls.thumbnails.show_details,
|
||||||
|
|||||||
@@ -51,7 +51,13 @@
|
|||||||
"update_seconds": "Refresh default view every X seconds (0=never)"
|
"update_seconds": "Refresh default view every X seconds (0=never)"
|
||||||
},
|
},
|
||||||
"event_gallery": {
|
"event_gallery": {
|
||||||
"min_columns": "Minimum number of columns"
|
"controls": {
|
||||||
|
"thumbnails": {
|
||||||
|
"size": "Event Gallery thumbnails size in pixels",
|
||||||
|
"show_details": "Show event details with thumbnails",
|
||||||
|
"show_controls": "Show event controls with thumbnails"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"event_viewer": {
|
"event_viewer": {
|
||||||
"auto_play": "Automatically play media",
|
"auto_play": "Automatically play media",
|
||||||
@@ -66,7 +72,7 @@
|
|||||||
"controls": {
|
"controls": {
|
||||||
"next_previous": {
|
"next_previous": {
|
||||||
"style": "Event Viewer next & previous control style",
|
"style": "Event Viewer next & previous control style",
|
||||||
"size": "Event Viewer next & previous control size (e.g. '48px')",
|
"size": "Event Viewer next & previous control size in pixels",
|
||||||
"styles": {
|
"styles": {
|
||||||
"thumbnails": "Thumbnails",
|
"thumbnails": "Thumbnails",
|
||||||
"chevrons": "Chevrons",
|
"chevrons": "Chevrons",
|
||||||
@@ -75,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
"thumbnails": {
|
"thumbnails": {
|
||||||
"mode": "Event Viewer thumbnails mode",
|
"mode": "Event Viewer thumbnails mode",
|
||||||
"size": "Event Viewer thumbnails size (e.g. '100px')",
|
"size": "Event Viewer thumbnails size in pixels",
|
||||||
"show_details": "Show event details with thumbnails",
|
"show_details": "Show event details with thumbnails",
|
||||||
"show_controls": "Show event controls with thumbnails",
|
"show_controls": "Show event controls with thumbnails",
|
||||||
"modes": {
|
"modes": {
|
||||||
@@ -109,7 +115,7 @@
|
|||||||
"controls": {
|
"controls": {
|
||||||
"next_previous": {
|
"next_previous": {
|
||||||
"style": "Live view next & previous control style",
|
"style": "Live view next & previous control style",
|
||||||
"size": "Live view next & previous control size (e.g. '48px')",
|
"size": "Live view next & previous control size in pixels",
|
||||||
"styles": {
|
"styles": {
|
||||||
"chevrons": "Chevrons",
|
"chevrons": "Chevrons",
|
||||||
"icons": "Icons",
|
"icons": "Icons",
|
||||||
@@ -118,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"thumbnails": {
|
"thumbnails": {
|
||||||
"mode": "Live thumbnails mode",
|
"mode": "Live thumbnails mode",
|
||||||
"size": "Live thumbnails size (e.g. '100px')",
|
"size": "Live thumbnails size in pixels",
|
||||||
"show_details": "Show event details with thumbnails",
|
"show_details": "Show event details with thumbnails",
|
||||||
"show_controls": "Show event controls with thumbnails",
|
"show_controls": "Show event controls with thumbnails",
|
||||||
"media": "Whether to show thumbnails of clips or snapshots",
|
"media": "Whether to show thumbnails of clips or snapshots",
|
||||||
@@ -169,7 +175,7 @@
|
|||||||
"above": "Above",
|
"above": "Above",
|
||||||
"below": "Below"
|
"below": "Below"
|
||||||
},
|
},
|
||||||
"button_size": "Menu button size (e.g. '40px')"
|
"button_size": "Menu button size in pixels"
|
||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"window_seconds": "The default length of the timeline view in seconds",
|
"window_seconds": "The default length of the timeline view in seconds",
|
||||||
@@ -183,7 +189,7 @@
|
|||||||
"controls": {
|
"controls": {
|
||||||
"thumbnails": {
|
"thumbnails": {
|
||||||
"mode": "Timeline thumbnails mode",
|
"mode": "Timeline thumbnails mode",
|
||||||
"size": "Timeline thumbnails size (e.g. '100px')",
|
"size": "Timeline thumbnails size in pixels",
|
||||||
"show_details": "Show event details with thumbnails",
|
"show_details": "Show event details with thumbnails",
|
||||||
"show_controls": "Show event controls with thumbnails"
|
"show_controls": "Show event controls with thumbnails"
|
||||||
}
|
}
|
||||||
@@ -235,7 +241,8 @@
|
|||||||
"event": {
|
"event": {
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"in_progress": "In Progress"
|
"in_progress": "In Progress",
|
||||||
|
"score": "Score"
|
||||||
},
|
},
|
||||||
"thumbnail": {
|
"thumbnail": {
|
||||||
"retain_indefinitely": "Event will be indefinitely retained",
|
"retain_indefinitely": "Event will be indefinitely retained",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
:host {
|
||||||
|
--frigate-card-thumbnail-size: 100px;
|
||||||
|
--frigate-card-thumbnail-size-max: 175px;
|
||||||
|
--frigate-card-thumbnail-details-width: calc(var(--frigate-card-thumbnail-size) + 200px);
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
ha-icon.favorite {
|
|
||||||
position: absolute;
|
|
||||||
color: gold;
|
|
||||||
background: rgba(0,0,0,0.2);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-icon.timeline {
|
|
||||||
position: absolute;
|
|
||||||
color: var(--primary-color);
|
|
||||||
right: 0px;
|
|
||||||
background: rgba(0,0,0,0.2);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
+14
-22
@@ -1,9 +1,6 @@
|
|||||||
@use '@material/image-list/mdc-image-list';
|
@use "const.scss";
|
||||||
@use '@material/image-list';
|
|
||||||
@use './favorite.scss';
|
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -13,6 +10,14 @@
|
|||||||
|
|
||||||
// Hide scrollbar: Firefox
|
// Hide scrollbar: Firefox
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
--frigate-card-gallery-gap: 3px;
|
||||||
|
--frigate-card-gallery-columns: 4;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--frigate-card-gallery-columns), minmax(0, 1fr));
|
||||||
|
grid-auto-rows: var(--frigate-card-thumbnail-size);
|
||||||
|
grid-column-gap: var(--frigate-card-gallery-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide scrollbar for Chrome, Safari and Opera
|
// Hide scrollbar for Chrome, Safari and Opera
|
||||||
@@ -20,21 +25,7 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.frigate-card-gallery {
|
ha-card {
|
||||||
// Note: In fullscreen, number of columns is overwritten in Javascript based
|
|
||||||
// on dimensions.
|
|
||||||
@include image-list.standard-columns(4, 5px);
|
|
||||||
@include image-list.shape-radius(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.frigate-card-gallery .mdc-image-list__image {
|
|
||||||
transition: transform 0.2s linear;
|
|
||||||
}
|
|
||||||
.frigate-card-gallery .mdc-image-list__image:hover {
|
|
||||||
transform: scale(1.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-card.frigate-card-gallery-folder {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -46,10 +37,11 @@ ha-card.frigate-card-gallery-folder {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: var(--primary-background-color, black);
|
background-color: var(--primary-background-color, black);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
height: 100%;
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon.favorite {
|
:host(:not([details])) ha-card,
|
||||||
opacity: 0.8;
|
:host(:not([details])) frigate-card-thumbnail {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@use "const.scss";
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
--frigate-card-carousel-thumbnail-opacity: 1.0;
|
--frigate-card-carousel-thumbnail-opacity: 1.0;
|
||||||
}
|
}
|
||||||
@@ -18,4 +20,13 @@
|
|||||||
}
|
}
|
||||||
.embla__slide.slide-selected {
|
.embla__slide.slide-selected {
|
||||||
opacity: 1.0;
|
opacity: 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
frigate-card-thumbnail[details] {
|
||||||
|
width: var(--frigate-card-thumbnail-details-width);
|
||||||
|
height: var(--frigate-card-thumbnail-size);
|
||||||
|
}
|
||||||
|
frigate-card-thumbnail:not([details]) {
|
||||||
|
width: var(--frigate-card-thumbnail-size);
|
||||||
|
height: var(--frigate-card-thumbnail-size);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 5px;
|
||||||
|
padding: 8px;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
overflow: hidden;
|
||||||
|
column-gap: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.right, div.left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.left {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-content: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
column-gap: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.left div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.heading {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.larger {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
+21
-32
@@ -1,15 +1,12 @@
|
|||||||
@use './favorite.scss';
|
@use "const.scss";
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
--frigate-card-thumbnail-size: 100px;
|
// Ensure control icons are relative to the thumbnail.
|
||||||
|
position: relative;
|
||||||
// Do not let the contain expand to fill more height than the height of the
|
|
||||||
// thumbnail. Without this the thumbnail carousel will allow the thumbnail to
|
|
||||||
// expand to the full height of the viewport on a vertical carousel.
|
|
||||||
max-height: var(--frigate-card-thumbnail-size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([details]) {
|
:host([details]) {
|
||||||
@@ -31,10 +28,11 @@ img {
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
||||||
// Restrict images to a maximum of thumbnail size.
|
// Restrict images to a maximum of thumbnail size.
|
||||||
max-width: var(--frigate-card-thumbnail-size);
|
aspect-ratio: 1 / 1;
|
||||||
max-height: var(--frigate-card-thumbnail-size);
|
height: 100%;
|
||||||
min-width: var(--frigate-card-thumbnail-size);
|
|
||||||
min-height: var(--frigate-card-thumbnail-size);
|
max-width: var(--frigate-card-thumbnail-size-max);
|
||||||
|
max-height: var(--frigate-card-thumbnail-size-max);
|
||||||
|
|
||||||
transition: transform 0.2s linear;
|
transition: transform 0.2s linear;
|
||||||
}
|
}
|
||||||
@@ -42,27 +40,18 @@ img:hover {
|
|||||||
transform: scale(1.04);
|
transform: scale(1.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
div.details {
|
ha-icon.favorite {
|
||||||
display: flex;
|
position: absolute;
|
||||||
|
color: gold;
|
||||||
width: 200px;
|
background: rgba(0, 0, 0, 0.2);
|
||||||
margin-left: 5px;
|
border-radius: 50%;
|
||||||
padding: 8px;
|
opacity: 0.8;
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
div.details div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
div.details div.left {
|
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.larger {
|
ha-icon.timeline {
|
||||||
font-size: 1.5rem;
|
position: absolute;
|
||||||
}
|
color: var(--primary-color);
|
||||||
|
right: 0px;
|
||||||
span.heading {
|
background: rgba(0, 0, 0, 0.2);
|
||||||
font-weight: bold;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-15
@@ -14,6 +14,13 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { deepRemoveDefaults } from './zod-util';
|
import { deepRemoveDefaults } from './zod-util';
|
||||||
|
|
||||||
|
// The min allowed size of buttons.
|
||||||
|
export const BUTTON_SIZE_MIN = 20;
|
||||||
|
|
||||||
|
// The min/max width thumbnail (Frigate returns a maximum of 175px).
|
||||||
|
export const THUMBNAIL_WIDTH_MAX = 175;
|
||||||
|
export const THUMBNAIL_WIDTH_MIN = 75;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'frigate-card-editor': LovelaceCardEditor;
|
'frigate-card-editor': LovelaceCardEditor;
|
||||||
@@ -66,9 +73,6 @@ export type LiveProvider = typeof LIVE_PROVIDERS[number];
|
|||||||
|
|
||||||
export class FrigateCardError extends Error {}
|
export class FrigateCardError extends Error {}
|
||||||
|
|
||||||
// The maximum width thumbnail Frigate returns
|
|
||||||
export const THUMBNAIL_WIDTH_MAX = 175;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action Types (for "Picture Elements" / Menu)
|
* Action Types (for "Picture Elements" / Menu)
|
||||||
*/
|
*/
|
||||||
@@ -442,7 +446,7 @@ export type ImageViewConfig = z.infer<typeof imageConfigSchema>;
|
|||||||
|
|
||||||
const thumbnailsControlSchema = z.object({
|
const thumbnailsControlSchema = z.object({
|
||||||
mode: z.enum(['none', 'above', 'below', 'left', 'right']),
|
mode: z.enum(['none', 'above', 'below', 'left', 'right']),
|
||||||
size: z.string().optional(),
|
size: z.number().min(THUMBNAIL_WIDTH_MIN).max(THUMBNAIL_WIDTH_MAX).optional(),
|
||||||
show_details: z.boolean().optional(),
|
show_details: z.boolean().optional(),
|
||||||
show_controls: z.boolean().optional(),
|
show_controls: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
@@ -454,7 +458,7 @@ export type ThumbnailsControlConfig = z.infer<typeof thumbnailsControlSchema>;
|
|||||||
|
|
||||||
const nextPreviousControlConfigSchema = z.object({
|
const nextPreviousControlConfigSchema = z.object({
|
||||||
style: z.enum(['none', 'chevrons', 'icons', 'thumbnails']),
|
style: z.enum(['none', 'chevrons', 'icons', 'thumbnails']),
|
||||||
size: z.string(),
|
size: z.number().min(BUTTON_SIZE_MIN),
|
||||||
});
|
});
|
||||||
export type NextPreviousControlConfig = z.infer<typeof nextPreviousControlConfigSchema>;
|
export type NextPreviousControlConfig = z.infer<typeof nextPreviousControlConfigSchema>;
|
||||||
|
|
||||||
@@ -491,12 +495,12 @@ const liveConfigDefault = {
|
|||||||
transition_effect: 'slide' as const,
|
transition_effect: 'slide' as const,
|
||||||
controls: {
|
controls: {
|
||||||
next_previous: {
|
next_previous: {
|
||||||
size: '48px',
|
size: 48,
|
||||||
style: 'chevrons' as const,
|
style: 'chevrons' as const,
|
||||||
},
|
},
|
||||||
thumbnails: {
|
thumbnails: {
|
||||||
media: 'clips' as const,
|
media: 'clips' as const,
|
||||||
size: '100px',
|
size: 100,
|
||||||
show_details: false,
|
show_details: false,
|
||||||
show_controls: true,
|
show_controls: true,
|
||||||
mode: 'none' as const,
|
mode: 'none' as const,
|
||||||
@@ -617,7 +621,7 @@ const menuConfigDefault = {
|
|||||||
frigate_ui: true,
|
frigate_ui: true,
|
||||||
fullscreen: true,
|
fullscreen: true,
|
||||||
},
|
},
|
||||||
button_size: '40px',
|
button_size: 40,
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuConfigSchema = z
|
const menuConfigSchema = z
|
||||||
@@ -637,7 +641,7 @@ const menuConfigSchema = z
|
|||||||
fullscreen: z.boolean().default(menuConfigDefault.buttons.fullscreen),
|
fullscreen: z.boolean().default(menuConfigDefault.buttons.fullscreen),
|
||||||
})
|
})
|
||||||
.default(menuConfigDefault.buttons),
|
.default(menuConfigDefault.buttons),
|
||||||
button_size: z.string().default(menuConfigDefault.button_size),
|
button_size: z.number().min(BUTTON_SIZE_MIN).default(menuConfigDefault.button_size),
|
||||||
})
|
})
|
||||||
.default(menuConfigDefault);
|
.default(menuConfigDefault);
|
||||||
export type MenuConfig = z.infer<typeof menuConfigSchema>;
|
export type MenuConfig = z.infer<typeof menuConfigSchema>;
|
||||||
@@ -653,11 +657,11 @@ const viewerConfigDefault = {
|
|||||||
transition_effect: 'slide' as const,
|
transition_effect: 'slide' as const,
|
||||||
controls: {
|
controls: {
|
||||||
next_previous: {
|
next_previous: {
|
||||||
size: '48px',
|
size: 48,
|
||||||
style: 'thumbnails' as const,
|
style: 'thumbnails' as const,
|
||||||
},
|
},
|
||||||
thumbnails: {
|
thumbnails: {
|
||||||
size: '100px',
|
size: 100,
|
||||||
mode: 'none' as const,
|
mode: 'none' as const,
|
||||||
show_details: false,
|
show_details: false,
|
||||||
show_controls: true,
|
show_controls: true,
|
||||||
@@ -672,7 +676,9 @@ const viewerNextPreviousControlConfigSchema = nextPreviousControlConfigSchema.ex
|
|||||||
style: z
|
style: z
|
||||||
.enum(['none', 'thumbnails', 'chevrons'])
|
.enum(['none', 'thumbnails', 'chevrons'])
|
||||||
.default(viewerConfigDefault.controls.next_previous.style),
|
.default(viewerConfigDefault.controls.next_previous.style),
|
||||||
size: z.string().default(viewerConfigDefault.controls.next_previous.size),
|
size: nextPreviousControlConfigSchema.shape.size.default(
|
||||||
|
viewerConfigDefault.controls.next_previous.size,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
export type ViewerNextPreviousControlConfig = z.infer<
|
export type ViewerNextPreviousControlConfig = z.infer<
|
||||||
typeof viewerNextPreviousControlConfigSchema
|
typeof viewerNextPreviousControlConfigSchema
|
||||||
@@ -729,12 +735,36 @@ export type ViewerConfig = z.infer<typeof viewerConfigSchema>;
|
|||||||
* Event gallery configuration section (clips, snapshots).
|
* Event gallery configuration section (clips, snapshots).
|
||||||
*/
|
*/
|
||||||
const galleryConfigDefault = {
|
const galleryConfigDefault = {
|
||||||
min_columns: 5,
|
controls: {
|
||||||
|
thumbnails: {
|
||||||
|
size: 100,
|
||||||
|
show_details: false,
|
||||||
|
show_controls: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const galleryConfigSchema = z
|
const galleryConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
min_columns: z.number().min(1).max(10).default(galleryConfigDefault.min_columns),
|
controls: z
|
||||||
|
.object({
|
||||||
|
thumbnails: thumbnailsControlSchema
|
||||||
|
// Gallery shows thumbnails "centrally" so no need for the mode.
|
||||||
|
.omit({ mode: true })
|
||||||
|
.extend({
|
||||||
|
size: thumbnailsControlSchema.shape.size.default(
|
||||||
|
galleryConfigDefault.controls.thumbnails.size,
|
||||||
|
),
|
||||||
|
show_details: thumbnailsControlSchema.shape.show_details.default(
|
||||||
|
galleryConfigDefault.controls.thumbnails.show_details,
|
||||||
|
),
|
||||||
|
show_controls: thumbnailsControlSchema.shape.show_controls.default(
|
||||||
|
galleryConfigDefault.controls.thumbnails.show_controls,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.default(galleryConfigDefault.controls.thumbnails),
|
||||||
|
})
|
||||||
|
.default(galleryConfigDefault.controls),
|
||||||
})
|
})
|
||||||
.merge(actionsSchema)
|
.merge(actionsSchema)
|
||||||
.default(galleryConfigDefault);
|
.default(galleryConfigDefault);
|
||||||
@@ -776,7 +806,7 @@ const timelineConfigDefault = {
|
|||||||
controls: {
|
controls: {
|
||||||
thumbnails: {
|
thumbnails: {
|
||||||
mode: 'left' as const,
|
mode: 'left' as const,
|
||||||
size: '100px' as const,
|
size: 100,
|
||||||
show_details: true,
|
show_details: true,
|
||||||
show_controls: true,
|
show_controls: true,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user