Initial version of media filter components.

This commit is contained in:
Dermot Duffy
2023-01-24 19:36:54 -08:00
parent d0a6f0928f
commit e5924db352
10 changed files with 431 additions and 15 deletions
+24 -14
View File
@@ -27,13 +27,13 @@ import { View } from '../view/view.js';
import { renderProgressIndicator } from './message.js'; import { renderProgressIndicator } from './message.js';
import './thumbnail.js'; import './thumbnail.js';
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js'; import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
import './media-filter';
import { createRef, ref, Ref } from 'lit/directives/ref.js'; import { createRef, ref, Ref } from 'lit/directives/ref.js';
import { MediaQueriesClassifier } from '../view/media-queries-classifier'; import { MediaQueriesClassifier } from '../view/media-queries-classifier';
import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries'; import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries';
import { EventQuery, MediaQuery, RecordingQuery } from '../camera/types'; import { EventQuery, MediaQuery, RecordingQuery } from '../camera/types';
import { MediaQueriesResults } from '../view/media-queries-results'; import { MediaQueriesResults } from '../view/media-queries-results';
import { errorToConsole } from '../utils/basic'; import { errorToConsole } from '../utils/basic';
import "./media-filter";
const GALLERY_MEDIA_CHUNK_SIZE = 100; const GALLERY_MEDIA_CHUNK_SIZE = 100;
@@ -101,15 +101,25 @@ export class FrigateCardGallery extends LitElement {
return renderProgressIndicator({ cardWideConfig: this.cardWideConfig }); return renderProgressIndicator({ cardWideConfig: this.cardWideConfig });
} }
// TODO Make this slot choice configuration left/right.
return html` return html`
<frigate-card-gallery-core <frigate-card-surround-basic>
.hass=${this.hass} <frigate-card-media-filter
.view=${this.view} .hass=${this.hass}
.galleryConfig=${this.galleryConfig} .cameras=${this.cameras}
.cameras=${this.cameras} .cameraManager=${this.cameraManager}
.cameraManager=${this.cameraManager} slot="right"
> >
</frigate-card-gallery-core> </frigate-card-media-filter>
<frigate-card-gallery-core
.hass=${this.hass}
.view=${this.view}
.galleryConfig=${this.galleryConfig}
.cameras=${this.cameras}
.cameraManager=${this.cameraManager}
>
</frigate-card-gallery-core>
</frigate-card-surround-basic>
`; `;
} }
@@ -146,7 +156,7 @@ export class FrigateCardGalleryCore extends LitElement {
protected _intersectionObserver: IntersectionObserver; protected _intersectionObserver: IntersectionObserver;
protected _resizeObserver: ResizeObserver; protected _resizeObserver: ResizeObserver;
protected _refSentinel: Ref<HTMLElement> = createRef(); protected _refLoader: Ref<HTMLElement> = createRef();
@state() @state()
protected _showExtensionLoader = true; protected _showExtensionLoader = true;
@@ -167,7 +177,7 @@ export class FrigateCardGalleryCore extends LitElement {
this._resizeObserver.observe(this); this._resizeObserver.observe(this);
// Request update in order to ensure the intersection observer reconnects // Request update in order to ensure the intersection observer reconnects
// with the sentinel. // with the loader sentinel.
this.requestUpdate(); this.requestUpdate();
} }
@@ -326,7 +336,7 @@ export class FrigateCardGalleryCore extends LitElement {
</frigate-card-thumbnail>`, </frigate-card-thumbnail>`,
)} )}
${this._showExtensionLoader ${this._showExtensionLoader
? html` <ha-card class="sentinel" ${ref(this._refSentinel)}> ? html` <ha-card ${ref(this._refLoader)}>
<span class="dotdotdot"></span> <span class="dotdotdot"></span>
</ha-card>` </ha-card>`
: ''} : ''}
@@ -334,9 +344,9 @@ export class FrigateCardGalleryCore extends LitElement {
} }
public updated(): void { public updated(): void {
if (this._refSentinel.value) { if (this._refLoader.value) {
this._intersectionObserver.disconnect(); this._intersectionObserver.disconnect();
this._intersectionObserver.observe(this._refSentinel.value); this._intersectionObserver.observe(this._refLoader.value);
} }
} }
+209
View File
@@ -0,0 +1,209 @@
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { createRef, ref, Ref } from 'lit/directives/ref.js';
import { DateRange } from '../camera/range';
import { localize } from '../localize/localize';
import mediaFilterStyle from '../scss/media-filter.scss';
import { ExtendedHomeAssistant } from '../types';
import { dispatchFrigateCardEvent } from '../utils/basic';
export interface ValueLabel<T> {
value?: T;
label: string;
}
export interface MediaFilterCoreSelection {
camera?: string[];
what?: string[];
where?: string[];
when?: MediaFilterCoreWhenSelection;
favorite?: MediaFilterCoreFavoriteSelection;
}
type FilterElement<T> = HTMLElement & {
selectedItem?: ValueLabel<T>;
};
export enum MediaFilterCoreFavoriteSelection {
All = 'all',
Favorite = 'favorite',
NotFavorite = 'not-favorite',
}
export enum MediaFilterCoreWhen {
All = 'all',
Today = 'today',
Yesterday = 'yesterday',
PastWeek = 'past-week',
PastMonth = 'past-month',
Custom = 'custom',
}
export interface MediaFilterCoreWhenSelection {
selection: MediaFilterCoreWhen;
custom?: DateRange;
}
@customElement('frigate-card-media-filter-core')
export class FrigateCardMediaFilterCore extends LitElement {
@property({ attribute: false })
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public whenOptions?: ValueLabel<MediaFilterCoreWhenSelection>[];
@property({ attribute: false })
public cameraOptions?: ValueLabel<string>[];
@property({ attribute: false })
public whatOptions?: ValueLabel<string>[];
@property({ attribute: false })
public whereOptions?: ValueLabel<string>[];
protected _refWhen: Ref<FilterElement<MediaFilterCoreWhenSelection>> = createRef();
protected _refCamera: Ref<FilterElement<string>> = createRef();
protected _refWhat: Ref<FilterElement<string>> = createRef();
protected _refWhere: Ref<FilterElement<string>> = createRef();
protected _refFavorite: Ref<FilterElement<MediaFilterCoreFavoriteSelection>> =
createRef();
protected _valueChangedHandler(ev: CustomEvent<{ value: unknown }>): void {
// Handler is called on initial load -- skip it.
if (!ev.detail.value) {
return;
}
const values: MediaFilterCoreSelection = {
...(this._refWhen.value && {
when: this._refWhen.value.selectedItem?.value as MediaFilterCoreWhenSelection,
}),
...(this._refCamera.value && {
camera: this._refCamera.value.selectedItem?.value
? [this._refCamera.value.selectedItem?.value]
: undefined,
}),
...(this._refWhat.value && {
what: this._refWhat.value.selectedItem?.value
? [this._refWhat.value.selectedItem?.value]
: undefined,
}),
...(this._refWhere.value && {
where: this._refWhere.value.selectedItem?.value
? [this._refWhere.value.selectedItem?.value]
: undefined,
}),
...(this._refFavorite.value && {
favorite: this._refFavorite.value.selectedItem?.value as
| MediaFilterCoreFavoriteSelection
| undefined,
}),
};
dispatchFrigateCardEvent(this, 'media-filter-core:change', values);
}
/**
* Master render method.
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
const favoriteOptions: ValueLabel<MediaFilterCoreFavoriteSelection>[] = [
{
value: MediaFilterCoreFavoriteSelection.All,
label: localize('media_filter.all'),
},
{
value: MediaFilterCoreFavoriteSelection.Favorite,
label: localize('media_filter.favorite'),
},
{
value: MediaFilterCoreFavoriteSelection.NotFavorite,
label: localize('media_filter.not_favorite'),
},
];
// Time based options are not pre-computed here to ensure relative dates
// (e.g. 'today') are always calculated when activated not when rendered.
const whenOptions = [
{
value: { selection: MediaFilterCoreWhen.All },
label: localize('media_filter.all'),
},
{
value: { selection: MediaFilterCoreWhen.Today },
label: localize('media_filter.today'),
},
{
value: { selection: MediaFilterCoreWhen.Yesterday },
label: localize('media_filter.yesterday'),
},
{
value: { selection: MediaFilterCoreWhen.PastWeek },
label: localize('media_filter.past_week'),
},
{
value: { selection: MediaFilterCoreWhen.PastMonth },
label: localize('media_filter.past_month'),
},
...(this.whenOptions ?? []),
];
return html`
<ha-combo-box
${ref(this._refWhen)}
.hass=${this.hass}
.label=${localize('media_filter.when')}
.items=${whenOptions}
.allowCustomValue=${false}
@value-changed=${this._valueChangedHandler.bind(this)}
></ha-combo-box>
${this.cameraOptions
? html` <ha-combo-box
${ref(this._refCamera)}
.hass=${this.hass}
.label=${localize('media_filter.camera')}
.items=${this.cameraOptions}
.allowCustomValue=${false}
@value-changed=${this._valueChangedHandler.bind(this)}
></ha-combo-box>`
: ''}
${this.whatOptions
? html` <ha-combo-box
${ref(this._refWhat)}
.hass=${this.hass}
.label=${localize('media_filter.what')}
.items=${this.whatOptions}
.allowCustomValue=${false}
@value-changed=${this._valueChangedHandler.bind(this)}
></ha-combo-box>`
: ''}
${this.whereOptions
? html`<ha-combo-box
${ref(this._refWhere)}
.hass=${this.hass}
.label=${localize('media_filter.where')}
.items=${this.whereOptions}
.allowCustomValue=${false}
@value-changed=${this._valueChangedHandler.bind(this)}
></ha-combo-box>`
: ''}
<ha-combo-box
${ref(this._refFavorite)}
.hass=${this.hass}
.label=${localize('media_filter.favorite')}
.items=${favoriteOptions}
.allowCustomValue=${false}
@value-changed=${this._valueChangedHandler.bind(this)}
></ha-combo-box>
`;
}
static get styles(): CSSResultGroup {
return unsafeCSS(mediaFilterStyle);
}
}
declare global {
interface HTMLElementTagNameMap {
'frigate-card-media-filter-core': FrigateCardMediaFilterCore;
}
}
+143
View File
@@ -0,0 +1,143 @@
import { sub } from 'date-fns';
import endOfDay from 'date-fns/endOfDay';
import endOfYesterday from 'date-fns/endOfYesterday';
import endOfToday from 'date-fns/esm/endOfToday';
import startOfToday from 'date-fns/esm/startOfToday';
import startOfYesterday from 'date-fns/startOfYesterday';
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { CameraManager } from '../camera/manager';
import { DateRange } from '../camera/range';
import { CameraConfig, ExtendedHomeAssistant } from '../types';
import { getCameraTitle } from '../utils/camera';
import {
MediaFilterCoreFavoriteSelection,
MediaFilterCoreSelection,
MediaFilterCoreWhen,
MediaFilterCoreWhenSelection,
ValueLabel,
} from './media-filter-core';
import './surround.js';
import './timeline-core.js';
@customElement('frigate-card-media-filter')
export class FrigateCardMediaFilter extends LitElement {
@property({ attribute: false })
public hass?: ExtendedHomeAssistant;
@property({ attribute: false })
public cameras?: Map<string, CameraConfig>;
@property({ attribute: false })
public cameraManager?: CameraManager;
protected _cameraOptions: ValueLabel<string>[] = [];
protected _convertWhenToDateRange(
value?: MediaFilterCoreWhenSelection,
): DateRange | null {
if (!value) {
return null;
}
if (value.selection === MediaFilterCoreWhen.Custom && value.custom) {
return value.custom;
}
const now = new Date();
switch (value.selection) {
case MediaFilterCoreWhen.Today:
return { start: startOfToday(), end: endOfToday() };
case MediaFilterCoreWhen.Yesterday:
return { start: startOfYesterday(), end: endOfYesterday() };
case MediaFilterCoreWhen.PastWeek:
return { start: sub(now, { days: 7 }), end: endOfDay(now) };
case MediaFilterCoreWhen.PastMonth:
return { start: sub(now, { months: 1 }), end: endOfDay(now) };
}
return null;
}
protected _convertFavoriteToBoolean(
value?: MediaFilterCoreFavoriteSelection,
): boolean | null {
if (!value || value === MediaFilterCoreFavoriteSelection.All) {
return null;
}
return value === MediaFilterCoreFavoriteSelection.Favorite;
}
protected _mediaFilterHandler(ev: CustomEvent<MediaFilterCoreSelection>): void {
const convertedTime = this._convertWhenToDateRange(ev.detail.when);
const convertedFavorite = this._convertFavoriteToBoolean(ev.detail.favorite);
const details = {
...ev.detail,
...(convertedTime && { when: convertedTime }),
...(convertedFavorite && { favorite: convertedFavorite }),
};
// TODO: remove.
console.debug('Received media filter choices:', details);
}
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('cameras') && this.cameras) {
this._cameraOptions = Array.from(this.cameras.entries()).map(
([cameraID, cameraConfig]) => ({
value: cameraID,
label: getCameraTitle(this.hass, cameraConfig),
}),
);
}
}
protected render(): TemplateResult | void {
// TODO Replace with real custom when options
// TODO Replace with real custom what options
// TODO Replace with real custom where options
const whereOptions = [{ value: 'steps', label: 'Front Steps' }];
const whatOptions = [
{ value: 'car', label: 'Car' },
{ value: 'person', label: 'Person' },
];
const whenOptions = [
{
value: {
selection: MediaFilterCoreWhen.Custom,
custom: { start: startOfToday(), end: endOfToday() },
},
label: 'December 2021',
},
];
return html` <frigate-card-media-filter-core
.hass=${this.hass}
.whenOptions=${whenOptions}
.cameraOptions=${this._cameraOptions}
.whatOptions=${whatOptions}
.whereOptions=${whereOptions}
@frigate-card:media-filter-core:change=${this._mediaFilterHandler.bind(this)}
>
</frigate-card-media-filter-core>`;
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'frigate-card-media-filter': FrigateCardMediaFilter;
}
}
+13
View File
@@ -364,6 +364,19 @@
"start": "Start", "start": "Start",
"seek": "Seek" "seek": "Seek"
}, },
"media_filter": {
"all": "All",
"camera": "Camera",
"favorite": "Favorite",
"not_favorite": "Not Favorite",
"past_month": "Past Month",
"past_week": "Past Week",
"today": "Today",
"what": "What",
"when": "When",
"where": "Where",
"yesterday": "Yesterday"
},
"recording": { "recording": {
"events": "Events", "events": "Events",
"seek": "Seek" "seek": "Seek"
+13
View File
@@ -335,6 +335,19 @@
"start": "Avvia", "start": "Avvia",
"seek": "Cercare" "seek": "Cercare"
}, },
"media_filter": {
"all": "",
"camera": "",
"favorite": "",
"not_favorite": "",
"past_month": "",
"past_week": "",
"today": "",
"what": "",
"when": "",
"where": "",
"yesterday": ""
},
"recording": { "recording": {
"events": "Eventi", "events": "Eventi",
"seek": "Cercare" "seek": "Cercare"
+13
View File
@@ -335,6 +335,19 @@
"start": "Início", "start": "Início",
"seek": "Procurar" "seek": "Procurar"
}, },
"media_filter": {
"all": "",
"camera": "",
"favorite": "",
"not_favorite": "",
"past_month": "",
"past_week": "",
"today": "",
"what": "",
"when": "",
"where": "",
"yesterday": ""
},
"recording": { "recording": {
"events": "Eventos", "events": "Eventos",
"seek": "Procurar" "seek": "Procurar"
+3 -1
View File
@@ -16,7 +16,7 @@
display: grid; display: grid;
grid-template-columns: repeat(var(--frigate-card-gallery-columns), minmax(0, 1fr)); grid-template-columns: repeat(var(--frigate-card-gallery-columns), minmax(0, 1fr));
grid-auto-rows: 1fr; grid-auto-rows: min-content;
gap: var(--frigate-card-gallery-gap); gap: var(--frigate-card-gallery-gap);
} }
@@ -26,6 +26,8 @@
} }
:host ha-card { :host ha-card {
min-width: 100%;
aspect-ratio: 1/1;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
+5
View File
@@ -0,0 +1,5 @@
:host {
display: flex;
flex-direction: column;
overflow: auto;
}
+7
View File
@@ -2,7 +2,14 @@
// - TODO: ts-prune https://camchenry.com/blog/deleting-dead-code-in-typescript // - TODO: ts-prune https://camchenry.com/blog/deleting-dead-code-in-typescript
// - TODO: getRecordingTitle should use getCameraTitle but need hass. // - TODO: getRecordingTitle should use getCameraTitle but need hass.
// - TODO: Take MediaQueries wrappers out of the camera manager. // - TODO: Take MediaQueries wrappers out of the camera manager.
// Gallery:
// - TODO: Event gallery show_details default does not work. // - TODO: Event gallery show_details default does not work.
// - TODO: Use spinner for gallery loading / cardwideconfig not dotdotdot
// - TODO: Handle all TODOs in media filter and media filter core.
// - TODO: Make media filter slot choice configurable (gallery.ts).
// - TODO: Configurable drawer icons (e.g. filter).
// - TODO: Filter panel expands from right can occasionally 'stick' open.
// Hard: // Hard:
// - TODO: Implement gallery. // - TODO: Implement gallery.
+1
View File
@@ -23,6 +23,7 @@
// imported by a custom card directly. // imported by a custom card directly.
"globalTags": [ "globalTags": [
"ha-card", "ha-card",
"ha-combo-box",
"ha-icon", "ha-icon",
"ha-icon-button", "ha-icon-button",
"ha-button-menu", "ha-button-menu",