Merge pull request #263 from dermotduffy/gallery-thumbnail-size
Allow gallery snapshot/clip column count to be configured
This commit is contained in:
@@ -324,6 +324,7 @@ 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`. |
|
||||||
| `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
|
||||||
|
|||||||
@@ -1195,6 +1195,7 @@ export class FrigateCard extends LitElement {
|
|||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.view=${this._view}
|
.view=${this._view}
|
||||||
.cameraConfig=${cameraConfig}
|
.cameraConfig=${cameraConfig}
|
||||||
|
.galleryConfig=${this._getConfig().event_gallery}
|
||||||
>
|
>
|
||||||
</frigate-card-gallery>`
|
</frigate-card-gallery>`
|
||||||
: ``}
|
: ``}
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import { HomeAssistant } from 'custom-card-helpers';
|
|||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
import type { CameraConfig, ExtendedHomeAssistant } from '../types.js';
|
import {
|
||||||
|
CameraConfig,
|
||||||
|
ExtendedHomeAssistant,
|
||||||
|
GalleryConfig,
|
||||||
|
frigateCardConfigDefaults,
|
||||||
|
} 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 { renderProgressIndicator } from './message.js';
|
import { renderProgressIndicator } from './message.js';
|
||||||
@@ -12,7 +17,6 @@ import { renderProgressIndicator } from './message.js';
|
|||||||
import galleryStyle from '../scss/gallery.scss';
|
import galleryStyle from '../scss/gallery.scss';
|
||||||
|
|
||||||
const MAX_THUMBNAIL_WIDTH = 175;
|
const MAX_THUMBNAIL_WIDTH = 175;
|
||||||
const DEFAULT_COLUMNS = 5;
|
|
||||||
|
|
||||||
@customElement('frigate-card-gallery')
|
@customElement('frigate-card-gallery')
|
||||||
export class FrigateCardGallery extends LitElement {
|
export class FrigateCardGallery extends LitElement {
|
||||||
@@ -25,6 +29,9 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected cameraConfig?: CameraConfig;
|
protected cameraConfig?: CameraConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected galleryConfig?: GalleryConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master render method.
|
* Master render method.
|
||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
@@ -55,7 +62,11 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<frigate-card-gallery-core .hass=${this.hass} .view=${this.view}>
|
<frigate-card-gallery-core
|
||||||
|
.hass=${this.hass}
|
||||||
|
.view=${this.view}
|
||||||
|
.galleryConfig=${this.galleryConfig}
|
||||||
|
>
|
||||||
</frigate-card-gallery-core>
|
</frigate-card-gallery-core>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -76,33 +87,50 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected view?: Readonly<View>;
|
protected view?: Readonly<View>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
protected galleryConfig?: GalleryConfig;
|
||||||
|
|
||||||
protected _resizeObserver: ResizeObserver;
|
protected _resizeObserver: ResizeObserver;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
protected _columns = DEFAULT_COLUMNS;
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component connected callback.
|
||||||
|
*/
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._resizeObserver?.observe(this);
|
this._resizeObserver?.observe(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component disconnected callback.
|
||||||
|
*/
|
||||||
disconnectedCallback(): void {
|
disconnectedCallback(): void {
|
||||||
this._resizeObserver.disconnect();
|
this._resizeObserver.disconnect();
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle gallery resize.
|
||||||
|
*/
|
||||||
protected _resizeHandler(): void {
|
protected _resizeHandler(): void {
|
||||||
this._columns = Math.max(
|
this._columns = Math.max(
|
||||||
DEFAULT_COLUMNS,
|
this.galleryConfig?.min_columns ??
|
||||||
|
frigateCardConfigDefaults.event_gallery.min_columns,
|
||||||
Math.ceil(this.clientWidth / MAX_THUMBNAIL_WIDTH),
|
Math.ceil(this.clientWidth / MAX_THUMBNAIL_WIDTH),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Master render method.
|
||||||
|
* @returns A rendered template.
|
||||||
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (
|
if (
|
||||||
!this.hass ||
|
!this.hass ||
|
||||||
@@ -114,14 +142,22 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = {
|
const itemStyle = {
|
||||||
// Controls the number of columns in the gallery (allows for 5px gutter).
|
// Controls the number of columns in the gallery (allows for 5px gutter).
|
||||||
width: `calc(${100 / this._columns}% - 5.25px)`,
|
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">
|
return html` <ul class="mdc-image-list frigate-card-gallery">
|
||||||
${this.view && this.view.previous
|
${this.view && this.view.previous
|
||||||
? html`<li class="mdc-image-list__item" style="${styleMap(styles)}">
|
? html`<li class="mdc-image-list__item" style="${styleMap(itemStyle)}">
|
||||||
<div class="mdc-image-list__image-aspect-container">
|
<div class="mdc-image-list__image-aspect-container">
|
||||||
<div class="mdc-image-list__image">
|
<div class="mdc-image-list__image">
|
||||||
<ha-card
|
<ha-card
|
||||||
@@ -141,7 +177,7 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
: ''}
|
: ''}
|
||||||
${this.view.target.children.map(
|
${this.view.target.children.map(
|
||||||
(child, index) =>
|
(child, index) =>
|
||||||
html` <li class="mdc-image-list__item" style="${styleMap(styles)}">
|
html` <li class="mdc-image-list__item" style="${styleMap(itemStyle)}">
|
||||||
<div class="mdc-image-list__image-aspect-container">
|
<div class="mdc-image-list__image-aspect-container">
|
||||||
${child.can_expand
|
${child.can_expand
|
||||||
? html`<div class="mdc-image-list__image">
|
? html`<div class="mdc-image-list__image">
|
||||||
@@ -159,7 +195,7 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
outlined=""
|
outlined=""
|
||||||
class="frigate-card-gallery-folder"
|
class="frigate-card-gallery-folder"
|
||||||
>
|
>
|
||||||
<div>${child.title}</div>
|
<div style="${styleMap(folderStyle)}">${child.title}</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
</div>`
|
</div>`
|
||||||
: child.thumbnail
|
: child.thumbnail
|
||||||
@@ -187,6 +223,9 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
</ul>`;
|
</ul>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get styles.
|
||||||
|
*/
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(galleryStyle);
|
return unsafeCSS(galleryStyle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export const CONF_VIEW_TIMEOUT = `${CONF_VIEW}.timeout` as const;
|
|||||||
export const CONF_VIEW_UPDATE_FORCE = `${CONF_VIEW}.update_force` as const;
|
export const CONF_VIEW_UPDATE_FORCE = `${CONF_VIEW}.update_force` as const;
|
||||||
export const CONF_VIEW_UPDATE_ENTITIES = `${CONF_VIEW}.update_entities` as const;
|
export const CONF_VIEW_UPDATE_ENTITIES = `${CONF_VIEW}.update_entities` as const;
|
||||||
|
|
||||||
|
export const CONF_EVENT_GALLERY = 'event_gallery' as const;
|
||||||
|
export const CONF_EVENT_GALLERY_MIN_COLUMNS = `${CONF_EVENT_GALLERY}.min_columns` 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_AUTOPLAY_CLIP =
|
export const CONF_EVENT_VIEWER_AUTOPLAY_CLIP =
|
||||||
`${CONF_EVENT_VIEWER}.autoplay_clip` as const;
|
`${CONF_EVENT_VIEWER}.autoplay_clip` as const;
|
||||||
|
|||||||
+50
-3
@@ -27,6 +27,7 @@ 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_VIEWER_AUTOPLAY_CLIP,
|
CONF_EVENT_VIEWER_AUTOPLAY_CLIP,
|
||||||
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,
|
||||||
@@ -125,6 +126,12 @@ const options: EditorOptions = {
|
|||||||
secondary: localize('editor.event_viewer_secondary'),
|
secondary: localize('editor.event_viewer_secondary'),
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
|
event_gallery: {
|
||||||
|
icon: 'grid',
|
||||||
|
name: localize('editor.event_gallery'),
|
||||||
|
secondary: localize('editor.event_gallery_secondary'),
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
image: {
|
image: {
|
||||||
icon: 'image',
|
icon: 'image',
|
||||||
name: localize('editor.image'),
|
name: localize('editor.image'),
|
||||||
@@ -254,6 +261,30 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _renderSlider(
|
||||||
|
configPath: string,
|
||||||
|
valueDefault: number,
|
||||||
|
icon: string,
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
): TemplateResult | void {
|
||||||
|
if (!this._config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const value = Number(getConfigValue(this._config, configPath, valueDefault));
|
||||||
|
return html`<ha-labeled-slider
|
||||||
|
caption=${this._getLabel(configPath)}
|
||||||
|
icon=${icon}
|
||||||
|
max=${max}
|
||||||
|
min=${min}
|
||||||
|
?pin=${true}
|
||||||
|
value=${isNaN(value) ? valueDefault : value}
|
||||||
|
@value-changed=${this._valueChangedHandler.bind(this)}
|
||||||
|
.configValue=${configPath}
|
||||||
|
>
|
||||||
|
</ha-labeled-slider>`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a camera header.
|
* Render a camera header.
|
||||||
* @param cameraIndex The index of the camera to edit/add.
|
* @param cameraIndex The index of the camera to edit/add.
|
||||||
@@ -506,10 +537,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The climate more-info has ha-switch and paper-dropdown-menu elements that
|
// The climate more-info loads ha-switch and paper-dropdown-menu.
|
||||||
// are lazy loaded unless explicitly loaded via climate here.
|
|
||||||
this._helpers.importMoreInfoControl('climate');
|
this._helpers.importMoreInfoControl('climate');
|
||||||
|
|
||||||
|
// The light more-info loads ha-labeled-slider.
|
||||||
|
this._helpers.importMoreInfoControl('light');
|
||||||
|
|
||||||
const cameraEntities = this._getEntities('camera');
|
const cameraEntities = this._getEntities('camera');
|
||||||
|
|
||||||
const viewModes = {
|
const viewModes = {
|
||||||
@@ -705,6 +738,18 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ''}
|
: ''}
|
||||||
|
${this._renderOptionSetHeader('event_gallery')}
|
||||||
|
${options.event_gallery.show
|
||||||
|
? html` <div class="values">
|
||||||
|
${this._renderSlider(
|
||||||
|
CONF_EVENT_GALLERY_MIN_COLUMNS,
|
||||||
|
defaults.event_gallery.min_columns,
|
||||||
|
"mdi:view-column",
|
||||||
|
1,
|
||||||
|
10,
|
||||||
|
)}
|
||||||
|
</div>`
|
||||||
|
: ''}
|
||||||
${this._renderOptionSetHeader('event_viewer')}
|
${this._renderOptionSetHeader('event_viewer')}
|
||||||
${options.event_viewer.show
|
${options.event_viewer.show
|
||||||
? html` <div class="values">
|
? html` <div class="values">
|
||||||
@@ -822,8 +867,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
let value;
|
let value;
|
||||||
if ('checked' in target) {
|
if ('checked' in target) {
|
||||||
value = target.checked;
|
value = target.checked;
|
||||||
} else {
|
} else if (typeof target.value === 'string') {
|
||||||
value = target.value?.trim();
|
value = target.value?.trim();
|
||||||
|
} else {
|
||||||
|
value = target.value;
|
||||||
}
|
}
|
||||||
const key: string = target.configValue;
|
const key: string = target.configValue;
|
||||||
if (!key) {
|
if (!key) {
|
||||||
|
|||||||
@@ -44,6 +44,9 @@
|
|||||||
"timeout": "View timeout secs (before returning to default, 0=never)",
|
"timeout": "View timeout secs (before returning to default, 0=never)",
|
||||||
"update_force": "Force card updates (ignore media playing / interaction)"
|
"update_force": "Force card updates (ignore media playing / interaction)"
|
||||||
},
|
},
|
||||||
|
"event_gallery": {
|
||||||
|
"min_columns": "Minimum number of columns"
|
||||||
|
},
|
||||||
"event_viewer": {
|
"event_viewer": {
|
||||||
"autoplay_clip": "Autoplay clips",
|
"autoplay_clip": "Autoplay clips",
|
||||||
"draggable": "Event Viewer can be dragged/swiped",
|
"draggable": "Event Viewer can be dragged/swiped",
|
||||||
@@ -144,8 +147,10 @@
|
|||||||
"menu_secondary": "Menu look & feel options",
|
"menu_secondary": "Menu look & feel options",
|
||||||
"live": "Live",
|
"live": "Live",
|
||||||
"live_secondary": "Live camera view options",
|
"live_secondary": "Live camera view options",
|
||||||
|
"event_gallery": "Event gallery",
|
||||||
|
"event_gallery_secondary": "Snapshots & clips gallery options",
|
||||||
"event_viewer": "Event viewer",
|
"event_viewer": "Event viewer",
|
||||||
"event_viewer_secondary": "Snapshots & clips gallery options",
|
"event_viewer_secondary": "Snapshots & clips viewer options",
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
"image_secondary": "Static image view options",
|
"image_secondary": "Static image view options",
|
||||||
"dimensions": "Dimensions",
|
"dimensions": "Dimensions",
|
||||||
|
|||||||
@@ -42,4 +42,5 @@ ha-card.frigate-card-gallery-folder {
|
|||||||
background-color: var(--primary-background-color, black);
|
background-color: var(--primary-background-color, black);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
+16
-7
@@ -611,8 +611,17 @@ export type ViewerConfig = z.infer<typeof viewerConfigSchema>;
|
|||||||
/**
|
/**
|
||||||
* Event gallery configuration section (clips, snapshots).
|
* Event gallery configuration section (clips, snapshots).
|
||||||
*/
|
*/
|
||||||
|
const galleryConfigDefault = {
|
||||||
|
min_columns: 5,
|
||||||
|
};
|
||||||
|
|
||||||
const galleryConfigSchema = actionsSchema.optional();
|
const galleryConfigSchema = z
|
||||||
|
.object({
|
||||||
|
min_columns: z.number().min(1).max(10).default(galleryConfigDefault.min_columns),
|
||||||
|
})
|
||||||
|
.merge(actionsSchema)
|
||||||
|
.default(galleryConfigDefault);
|
||||||
|
export type GalleryConfig = z.infer<typeof galleryConfigSchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dimensions configuration section.
|
* Dimensions configuration section.
|
||||||
@@ -644,12 +653,11 @@ const dimensionsConfigSchema = z
|
|||||||
* Configuration overrides
|
* Configuration overrides
|
||||||
*/
|
*/
|
||||||
// Strip all defaults from the override schemas, to ensure values are only what
|
// Strip all defaults from the override schemas, to ensure values are only what
|
||||||
// the user has specified.
|
// the user has specified.
|
||||||
const overrideConfigurationSchema =
|
const overrideConfigurationSchema = z.object({
|
||||||
z.object({
|
live: deepRemoveDefaults(liveOverridableConfigSchema).optional(),
|
||||||
live: deepRemoveDefaults(liveOverridableConfigSchema).optional(),
|
menu: deepRemoveDefaults(menuConfigSchema).optional(),
|
||||||
menu: deepRemoveDefaults(menuConfigSchema).optional(),
|
});
|
||||||
});
|
|
||||||
export type OverrideConfigurationKey = keyof z.infer<typeof overrideConfigurationSchema>;
|
export type OverrideConfigurationKey = keyof z.infer<typeof overrideConfigurationSchema>;
|
||||||
|
|
||||||
const overridesSchema = z
|
const overridesSchema = z
|
||||||
@@ -701,6 +709,7 @@ export const frigateCardConfigDefaults = {
|
|||||||
menu: menuConfigDefault,
|
menu: menuConfigDefault,
|
||||||
live: liveConfigDefault,
|
live: liveConfigDefault,
|
||||||
event_viewer: viewerConfigDefault,
|
event_viewer: viewerConfigDefault,
|
||||||
|
event_gallery: galleryConfigDefault,
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuButtonSchema = z.union([
|
const menuButtonSchema = z.union([
|
||||||
|
|||||||
Reference in New Issue
Block a user