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