diff --git a/package.json b/package.json
index 91ddb3fd..65585378 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,6 @@
"@cycjimmy/jsmpeg-player": "^5.1.1",
"@egjs/hammerjs": "^2.0.17",
"@lit-labs/task": "^1.1.1",
- "@material/image-list": "^13.0.0",
"@material/mwc-menu": "^0.25.3",
"@material/rtl": "^13.0.0",
"@types/bluebird": "^3.5.36",
@@ -32,7 +31,7 @@
"keycharm": "^0.4.0",
"lit": "^2.2.1",
"lodash-es": "^4.17.21",
- "moment": "^2.29.1",
+ "moment": "^2.29.2",
"propagating-hammerjs": "^2.0.1",
"quick-lru": "^6.1.0",
"screenfull": "^6.0.1",
diff --git a/src/components/gallery.ts b/src/components/gallery.ts
index b83c8382..e3960962 100644
--- a/src/components/gallery.ts
+++ b/src/components/gallery.ts
@@ -1,22 +1,35 @@
+// TODO: Add details & controls & size support
+// TODO: Remove mini support if it's not actually needed?
+// TODO: automatic upgrade for thumbnail sizes in px
+// TODO: automatic remove of min_columns
+
/* 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 { customElement, property, state } from 'lit/decorators.js';
-import { styleMap } from 'lit/directives/style-map.js';
+import { customElement, property } from 'lit/decorators.js';
import {
CameraConfig,
ExtendedHomeAssistant,
GalleryConfig,
- THUMBNAIL_WIDTH_MAX,
frigateCardConfigDefaults,
} from '../types.js';
import { BrowseMediaUtil } from '../browse-media-util.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 { stopEventFromActivatingCardWideActions } from '../common.js';
+import './thumbnail.js';
+
import galleryStyle from '../scss/gallery.scss';
@customElement('frigate-card-gallery')
@@ -77,7 +90,13 @@ export class FrigateCardGallery extends LitElement {
* Get element styles.
*/
static get styles(): CSSResultGroup {
- return unsafeCSS(galleryStyle);
+ return css`
+ :host {
+ display: block;
+ width: 100%;
+ height: 100%;
+ }
+ `;
}
}
@@ -94,9 +113,6 @@ export class FrigateCardGalleryCore extends LitElement {
protected _resizeObserver: ResizeObserver;
- @state()
- protected _columns = frigateCardConfigDefaults.event_gallery.min_columns;
-
constructor() {
super();
this._resizeObserver = new ResizeObserver(this._resizeHandler.bind(this));
@@ -122,10 +138,16 @@ export class FrigateCardGalleryCore extends LitElement {
* Handle gallery resize.
*/
protected _resizeHandler(): void {
- this._columns = Math.max(
- this.galleryConfig?.min_columns ??
- frigateCardConfigDefaults.event_gallery.min_columns,
- Math.ceil(this.clientWidth / THUMBNAIL_WIDTH_MAX),
+ const thumbnailSize =
+ this.galleryConfig?.controls.thumbnails.size ??
+ frigateCardConfigDefaults.event_gallery.controls.thumbnails.size;
+ 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 +163,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.
* @returns A rendered template.
@@ -156,93 +198,67 @@ export class FrigateCardGalleryCore extends LitElement {
return html``;
}
- 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`
+ return html`
${this._showBackArrow()
- ? html`-
-
-
- {
- if (this.view && this.view.previous) {
- this.view.previous.dispatchChangeEvent(this);
- }
- stopEventFromActivatingCardWideActions(ev);
- }}
- outlined=""
- class="frigate-card-gallery-folder"
- >
-
-
-
-
- `
+ ? html` {
+ if (this.view && this.view.previous) {
+ this.view.previous.dispatchChangeEvent(this);
+ }
+ stopEventFromActivatingCardWideActions(ev);
+ }}
+ outlined=""
+ >
+
+ `
: ''}
${this.view.target.children.map(
(child, index) =>
- html` -
-
- ${child.can_expand
- ? html`
-
{
- if (this.hass && this.view) {
- BrowseMediaUtil.fetchChildMediaAndDispatchViewChange(
- this,
- this.hass,
- this.view,
- child,
- );
- }
- stopEventFromActivatingCardWideActions(ev);
- }}
- outlined=""
- class="frigate-card-gallery-folder"
- >
- ${child.title}
-
-
`
- : child.thumbnail
- ? html`

{
- if (this.view) {
- this.view
- .evolve({
- view: this.view.is('clips') ? 'clip' : 'snapshot',
- childIndex: index,
- })
- .dispatchChangeEvent(this);
- }
- stopEventFromActivatingCardWideActions(ev);
- }}
- />${child.frigate?.event?.retain_indefinitely
- ? html`
`
- : ``}`
- : ``}
-
- `,
+ html`
+ ${child.can_expand
+ ? html`
+ {
+ if (this.hass && this.view) {
+ BrowseMediaUtil.fetchChildMediaAndDispatchViewChange(
+ this,
+ this.hass,
+ this.view,
+ child,
+ );
+ }
+ stopEventFromActivatingCardWideActions(ev);
+ }}
+ outlined=""
+ class="foo"
+ >
+ ${child.title}
+
+ `
+ : child.thumbnail
+ ? html` {
+ if (this.view) {
+ this.view
+ .evolve({
+ view: this.view.is('clips') ? 'clip' : 'snapshot',
+ childIndex: index,
+ })
+ .dispatchChangeEvent(this);
+ }
+ stopEventFromActivatingCardWideActions(ev);
+ }}
+ >
+ `
+ : ``}
+ `,
)}
-
`;
+ `;
}
/**
diff --git a/src/components/next-prev-control.ts b/src/components/next-prev-control.ts
index 6c37f651..ec9882ac 100644
--- a/src/components/next-prev-control.ts
+++ b/src/components/next-prev-control.ts
@@ -13,7 +13,7 @@ export class FrigateCardNextPreviousControl extends LitElement {
set controlConfig(controlConfig: NextPreviousControlConfig | undefined) {
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;
}
@@ -31,7 +31,7 @@ export class FrigateCardNextPreviousControl extends LitElement {
public disabled = false;
// Label that is used for ARIA support and as tooltip.
- @property() label = "";
+ @property() label = '';
protected render(): TemplateResult {
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)) {
let icon: string;
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 {
if (!this.icon) {
return html``;
}
- icon = this.icon
+ icon = this.icon;
}
- return html`
+ return html`
`;
}
diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts
index 669d70d7..ae05d5de 100644
--- a/src/components/thumbnail-carousel.ts
+++ b/src/components/thumbnail-carousel.ts
@@ -4,7 +4,6 @@ import { EmblaOptionsType } from 'embla-carousel';
import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
-import { isEqual } from 'lodash-es';
import type {
FrigateBrowseMediaSource,
diff --git a/src/components/thumbnail.ts b/src/components/thumbnail.ts
index 346a24d5..60bc9906 100644
--- a/src/components/thumbnail.ts
+++ b/src/components/thumbnail.ts
@@ -12,6 +12,48 @@ import {
import { localize } from '../localize/localize.js';
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`
+
${prettifyFrigateName(this.event.label)}
+
+
+ ${localize('event.start')}:
+ ${format(fromUnixTime(this.event.start_time), 'HH:mm:ss')}
+
+
+
+
+ ${localize('event.duration')}:
+ ${getEventDurationString(this.event)}
+
+
+
+ `;
+ }
+
+ /**
+ * Get element styles.
+ */
+ static get styles(): CSSResult {
+ return unsafeCSS(thumbnailDetailsStyle);
+ }
+}
@customElement('frigate-card-thumbnail')
export class FrigateCardThumbnail extends LitElement {
@@ -21,9 +63,9 @@ export class FrigateCardThumbnail extends LitElement {
@property({ attribute: true, type: Boolean })
public controls = false;
- @property({ attribute: true })
- set thumbnail_size(size: string) {
- this.style.setProperty('--frigate-card-thumbnail-size', String(size));
+ @property({ attribute: true, type: Number })
+ set thumbnail_size(size: number) {
+ this.style.setProperty('--frigate-card-thumbnail-size', `${size}px`);
}
// ============================
@@ -33,7 +75,7 @@ export class FrigateCardThumbnail extends LitElement {
protected view?: Readonly;
@property({ attribute: false })
- public target?: FrigateBrowseMediaSource;
+ public target?: FrigateBrowseMediaSource | null;
@property({ attribute: false })
public childIndex?: number;
@@ -91,26 +133,9 @@ export class FrigateCardThumbnail extends LitElement {
/>`
: ``}
${this.details && event
- ? html`
-
-
${prettifyFrigateName(event.label)}
-
-
- ${localize('event.start')}:
- ${format(fromUnixTime(event.start_time), 'HH:mm:ss')}
-
-
-
-
- ${localize('event.duration')}:
- ${getEventDurationString(event)}
-
-
-
-
-
${(event.top_score * 100).toFixed(2) + '%'}
-
-
`
+ ? html``
: html``}
${this.controls
? html`;
const thumbnailsControlSchema = z.object({
mode: z.enum(['none', 'above', 'below', 'left', 'right']),
- size: z.string().optional(),
+ size: z.number().min(1).max(THUMBNAIL_WIDTH_MAX).optional(),
show_details: z.boolean().optional(),
show_controls: z.boolean().optional(),
});
@@ -454,7 +454,7 @@ export type ThumbnailsControlConfig = z.infer;
const nextPreviousControlConfigSchema = z.object({
style: z.enum(['none', 'chevrons', 'icons', 'thumbnails']),
- size: z.string(),
+ size: z.number().min(1),
});
export type NextPreviousControlConfig = z.infer;
@@ -491,12 +491,12 @@ const liveConfigDefault = {
transition_effect: 'slide' as const,
controls: {
next_previous: {
- size: '48px',
+ size: 48,
style: 'chevrons' as const,
},
thumbnails: {
media: 'clips' as const,
- size: '100px',
+ size: 100,
show_details: false,
show_controls: true,
mode: 'none' as const,
@@ -653,11 +653,11 @@ const viewerConfigDefault = {
transition_effect: 'slide' as const,
controls: {
next_previous: {
- size: '48px',
+ size: 48,
style: 'thumbnails' as const,
},
thumbnails: {
- size: '100px',
+ size: 100,
mode: 'none' as const,
show_details: false,
show_controls: true,
@@ -672,7 +672,9 @@ const viewerNextPreviousControlConfigSchema = nextPreviousControlConfigSchema.ex
style: z
.enum(['none', 'thumbnails', 'chevrons'])
.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<
typeof viewerNextPreviousControlConfigSchema
@@ -729,12 +731,36 @@ export type ViewerConfig = z.infer;
* Event gallery configuration section (clips, snapshots).
*/
const galleryConfigDefault = {
- min_columns: 5,
+ controls: {
+ thumbnails: {
+ size: 100,
+ show_details: false,
+ show_controls: false,
+ },
+ },
};
const galleryConfigSchema = z
.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)
.default(galleryConfigDefault);
@@ -776,7 +802,7 @@ const timelineConfigDefault = {
controls: {
thumbnails: {
mode: 'left' as const,
- size: '100px' as const,
+ size: 100,
show_details: true,
show_controls: true,
},