From ba196ff003c3d4937ab8ac7e8ac08606efd25fef Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 16 Apr 2022 14:59:23 -0700 Subject: [PATCH] First version of gallery refactor to use thumbnails. --- package.json | 3 +- src/components/gallery.ts | 206 +++++++++++++++------------ src/components/next-prev-control.ts | 13 +- src/components/thumbnail-carousel.ts | 1 - src/components/thumbnail.ts | 73 ++++++---- src/localize/languages/en.json | 3 +- src/scss/favorite.scss | 14 -- src/scss/gallery.scss | 37 ++--- src/scss/thumbnail-details.scss | 26 ++++ src/scss/thumbnail.scss | 52 +++---- src/types.ts | 52 +++++-- 11 files changed, 267 insertions(+), 213 deletions(-) delete mode 100644 src/scss/favorite.scss create mode 100644 src/scss/thumbnail-details.scss 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` `; + `; } /** 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)} + +
+
+
+
${score}
+
`; + } + + /** + * 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, },