Merge pull request #513 from dermotduffy/thumbnails-in-gallery

Add gallery support for showing thumbnail details
This commit is contained in:
Dermot Duffy
2022-04-17 20:07:17 -07:00
committed by GitHub
18 changed files with 494 additions and 278 deletions
+9 -7
View File
@@ -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
View File
@@ -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",
+68 -57
View File
@@ -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,25 +193,9 @@ 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">
<div class="mdc-image-list__image">
<ha-card
@click=${(ev) => { @click=${(ev) => {
if (this.view && this.view.previous) { if (this.view && this.view.previous) {
this.view.previous.dispatchChangeEvent(this); this.view.previous.dispatchChangeEvent(this);
@@ -182,20 +203,15 @@ export class FrigateCardGalleryCore extends LitElement {
stopEventFromActivatingCardWideActions(ev); stopEventFromActivatingCardWideActions(ev);
}} }}
outlined="" outlined=""
class="frigate-card-gallery-folder"
> >
<ha-icon .icon=${'mdi:arrow-left'}></ha-icon> <ha-icon .icon=${'mdi:arrow-left'}></ha-icon>
</ha-card> </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`<div class="mdc-image-list__image"> ? html`
<ha-card <ha-card
@click=${(ev) => { @click=${(ev) => {
if (this.hass && this.view) { if (this.hass && this.view) {
@@ -209,17 +225,18 @@ export class FrigateCardGalleryCore extends LitElement {
stopEventFromActivatingCardWideActions(ev); stopEventFromActivatingCardWideActions(ev);
}} }}
outlined="" outlined=""
class="frigate-card-gallery-folder" class="foo"
> >
<div style="${styleMap(folderStyle)}">${child.title}</div> <div>${child.title}</div>
</ha-card> </ha-card>
</div>` `
: child.thumbnail : child.thumbnail
? html`<img ? html`<frigate-card-thumbnail
aria-label="${child.title}" .view=${this.view}
class="mdc-image-list__image" .target=${this.view?.target ?? null}
src="${child.thumbnail}" .childIndex=${index}
title="${child.title}" ?details=${!!this.galleryConfig?.controls.thumbnails.show_details}
?controls=${!!this.galleryConfig?.controls.thumbnails.show_controls}
@click=${(ev: Event) => { @click=${(ev: Event) => {
if (this.view) { if (this.view) {
this.view this.view
@@ -231,18 +248,12 @@ export class FrigateCardGalleryCore extends LitElement {
} }
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>`; `;
} }
/** /**
+4 -1
View File
@@ -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);
+4 -7
View File
@@ -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') {
@@ -55,13 +55,10 @@ export class FrigateCardNextPreviousControl extends LitElement {
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>`;
} }
-1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+15 -8
View File
@@ -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",
+5
View File
@@ -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);
}
-14
View File
@@ -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
View File
@@ -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;
} }
+11
View File
@@ -1,3 +1,5 @@
@use "const.scss";
:host { :host {
--frigate-card-carousel-thumbnail-opacity: 1.0; --frigate-card-carousel-thumbnail-opacity: 1.0;
} }
@@ -19,3 +21,12 @@
.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);
}
+41
View File
@@ -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
View File
@@ -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
View File
@@ -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,
}, },