Add initial media filtering for the gallery.
This commit is contained in:
@@ -115,6 +115,8 @@ export class FrigateCardGallery extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.cameras=${this.cameras}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.view=${this.view}
|
||||
.mediaLimit=${GALLERY_MEDIA_CHUNK_SIZE}
|
||||
slot="right"
|
||||
>
|
||||
</frigate-card-media-filter>
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
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';
|
||||
@@ -13,9 +20,9 @@ export interface ValueLabel<T> {
|
||||
}
|
||||
|
||||
export interface MediaFilterCoreSelection {
|
||||
camera?: string[];
|
||||
what?: string[];
|
||||
where?: string[];
|
||||
cameraIDs?: Set<string>;
|
||||
what?: Set<string>;
|
||||
where?: Set<string>;
|
||||
when?: MediaFilterCoreWhenSelection;
|
||||
favorite?: MediaFilterCoreFavoriteSelection;
|
||||
}
|
||||
@@ -25,13 +32,11 @@ type FilterElement<T> = HTMLElement & {
|
||||
};
|
||||
|
||||
export enum MediaFilterCoreFavoriteSelection {
|
||||
All = 'all',
|
||||
Favorite = 'favorite',
|
||||
NotFavorite = 'not-favorite',
|
||||
}
|
||||
|
||||
export enum MediaFilterCoreWhen {
|
||||
All = 'all',
|
||||
Today = 'today',
|
||||
Yesterday = 'yesterday',
|
||||
PastWeek = 'past-week',
|
||||
@@ -50,10 +55,10 @@ export class FrigateCardMediaFilterCore extends LitElement {
|
||||
public hass?: ExtendedHomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public whenOptions?: ValueLabel<MediaFilterCoreWhenSelection>[];
|
||||
public cameraOptions?: ValueLabel<string>[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraOptions?: ValueLabel<string>[];
|
||||
public whenOptions?: ValueLabel<MediaFilterCoreWhenSelection>[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public whatOptions?: ValueLabel<string>[];
|
||||
@@ -61,54 +66,27 @@ export class FrigateCardMediaFilterCore extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public whereOptions?: ValueLabel<string>[];
|
||||
|
||||
protected _refWhen: Ref<FilterElement<MediaFilterCoreWhenSelection>> = createRef();
|
||||
@property({ attribute: false })
|
||||
public defaults?: MediaFilterCoreSelection;
|
||||
|
||||
protected _cameraOptions?: ValueLabel<string>[];
|
||||
protected _whenOptions?: ValueLabel<MediaFilterCoreWhenSelection>[];
|
||||
protected _whatOptions?: ValueLabel<string>[];
|
||||
protected _whereOptions?: ValueLabel<string>[];
|
||||
protected _favoriteOptions: ValueLabel<MediaFilterCoreFavoriteSelection>[];
|
||||
|
||||
protected _refCamera: Ref<FilterElement<string>> = createRef();
|
||||
protected _refWhen: Ref<FilterElement<MediaFilterCoreWhenSelection>> = 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>[] = [
|
||||
constructor() {
|
||||
super();
|
||||
this._favoriteOptions = [
|
||||
{
|
||||
value: MediaFilterCoreFavoriteSelection.All,
|
||||
value: undefined,
|
||||
label: localize('media_filter.all'),
|
||||
},
|
||||
{
|
||||
@@ -120,39 +98,100 @@ export class FrigateCardMediaFilterCore extends LitElement {
|
||||
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 ?? []),
|
||||
];
|
||||
protected _valueChangedHandler(ev: CustomEvent<{ value: unknown }>): void {
|
||||
// Handler is called on initial load -- skip it.
|
||||
if (!ev.detail.value) {
|
||||
return;
|
||||
}
|
||||
const values: MediaFilterCoreSelection = {
|
||||
...(this._refCamera.value &&
|
||||
this._refCamera.value.selectedItem?.value && {
|
||||
cameraIDs: new Set([this._refCamera.value.selectedItem.value]),
|
||||
}),
|
||||
...(this._refWhen.value &&
|
||||
this._refWhen.value.selectedItem?.value && {
|
||||
when: this._refWhen.value.selectedItem?.value,
|
||||
}),
|
||||
...(this._refWhat.value &&
|
||||
this._refWhat.value.selectedItem?.value && {
|
||||
what: new Set([this._refWhat.value.selectedItem.value]),
|
||||
}),
|
||||
...(this._refWhere.value &&
|
||||
this._refWhere.value.selectedItem?.value && {
|
||||
where: new Set([this._refWhere.value.selectedItem?.value]),
|
||||
}),
|
||||
...(this._refFavorite.value &&
|
||||
this._refFavorite.value.selectedItem?.value !== undefined && {
|
||||
favorite: this._refFavorite.value.selectedItem?.value,
|
||||
}),
|
||||
};
|
||||
dispatchFrigateCardEvent(this, 'media-filter-core:change', values);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('cameraOptions')) {
|
||||
this._cameraOptions = [
|
||||
{
|
||||
value: undefined,
|
||||
label: localize('media_filter.all'),
|
||||
},
|
||||
...(this.cameraOptions ?? []),
|
||||
];
|
||||
}
|
||||
if (changedProps.has('whenOptions')) {
|
||||
// Time based options are not pre-computed here to ensure relative dates
|
||||
// (e.g. 'today') are always calculated when activated not when rendered.
|
||||
this._whenOptions = [
|
||||
{
|
||||
value: undefined,
|
||||
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 ?? []),
|
||||
];
|
||||
}
|
||||
if (changedProps.has('whatOptions')) {
|
||||
this._whatOptions = [
|
||||
{ value: undefined, label: localize('media_filter.all') },
|
||||
...(this.whatOptions ?? []),
|
||||
];
|
||||
}
|
||||
if (changedProps.has('whereOptions')) {
|
||||
this._whereOptions = [
|
||||
{ value: undefined, label: localize('media_filter.all') },
|
||||
...(this.whereOptions ?? []),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
${ref(this._refWhen)}
|
||||
.hass=${this.hass}
|
||||
.label=${localize('media_filter.when')}
|
||||
.items=${whenOptions}
|
||||
.items=${this._whenOptions}
|
||||
.allowCustomValue=${false}
|
||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
||||
></ha-combo-box>
|
||||
@@ -161,7 +200,7 @@ export class FrigateCardMediaFilterCore extends LitElement {
|
||||
${ref(this._refCamera)}
|
||||
.hass=${this.hass}
|
||||
.label=${localize('media_filter.camera')}
|
||||
.items=${this.cameraOptions}
|
||||
.items=${this._cameraOptions}
|
||||
.allowCustomValue=${false}
|
||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
||||
></ha-combo-box>`
|
||||
@@ -171,7 +210,7 @@ export class FrigateCardMediaFilterCore extends LitElement {
|
||||
${ref(this._refWhat)}
|
||||
.hass=${this.hass}
|
||||
.label=${localize('media_filter.what')}
|
||||
.items=${this.whatOptions}
|
||||
.items="${this._whatOptions}"
|
||||
.allowCustomValue=${false}
|
||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
||||
></ha-combo-box>`
|
||||
@@ -181,7 +220,7 @@ export class FrigateCardMediaFilterCore extends LitElement {
|
||||
${ref(this._refWhere)}
|
||||
.hass=${this.hass}
|
||||
.label=${localize('media_filter.where')}
|
||||
.items=${this.whereOptions}
|
||||
.items=${this._whereOptions}
|
||||
.allowCustomValue=${false}
|
||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
||||
></ha-combo-box>`
|
||||
@@ -190,7 +229,7 @@ export class FrigateCardMediaFilterCore extends LitElement {
|
||||
${ref(this._refFavorite)}
|
||||
.hass=${this.hass}
|
||||
.label=${localize('media_filter.favorite')}
|
||||
.items=${favoriteOptions}
|
||||
.items=${this._favoriteOptions}
|
||||
.allowCustomValue=${false}
|
||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
||||
></ha-combo-box>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { sub } from 'date-fns';
|
||||
import sub from 'date-fns/sub';
|
||||
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 startOfDay from 'date-fns/startOfDay';
|
||||
import startOfYesterday from 'date-fns/startOfYesterday';
|
||||
import {
|
||||
css,
|
||||
@@ -17,6 +18,7 @@ import { CameraManager } from '../camera/manager';
|
||||
import { DateRange } from '../camera/range';
|
||||
import { CameraConfig, ExtendedHomeAssistant } from '../types';
|
||||
import { getCameraTitle } from '../utils/camera';
|
||||
import { View } from '../view/view';
|
||||
import {
|
||||
MediaFilterCoreFavoriteSelection,
|
||||
MediaFilterCoreSelection,
|
||||
@@ -26,6 +28,9 @@ import {
|
||||
} from './media-filter-core';
|
||||
import './surround.js';
|
||||
import './timeline-core.js';
|
||||
import { EventQuery, QueryType } from '../camera/types';
|
||||
import { EventMediaQueries } from '../view/media-queries';
|
||||
import { createViewForEvents } from '../utils/media-to-view.js';
|
||||
|
||||
@customElement('frigate-card-media-filter')
|
||||
export class FrigateCardMediaFilter extends LitElement {
|
||||
@@ -38,6 +43,12 @@ export class FrigateCardMediaFilter extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public view?: View;
|
||||
|
||||
@property({ attribute: false })
|
||||
public mediaLimit?: number;
|
||||
|
||||
protected _cameraOptions: ValueLabel<string>[] = [];
|
||||
|
||||
protected _convertWhenToDateRange(
|
||||
@@ -46,9 +57,6 @@ export class FrigateCardMediaFilter extends LitElement {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
if (value.selection === MediaFilterCoreWhen.Custom && value.custom) {
|
||||
return value.custom;
|
||||
}
|
||||
const now = new Date();
|
||||
switch (value.selection) {
|
||||
case MediaFilterCoreWhen.Today:
|
||||
@@ -56,9 +64,13 @@ export class FrigateCardMediaFilter extends LitElement {
|
||||
case MediaFilterCoreWhen.Yesterday:
|
||||
return { start: startOfYesterday(), end: endOfYesterday() };
|
||||
case MediaFilterCoreWhen.PastWeek:
|
||||
return { start: sub(now, { days: 7 }), end: endOfDay(now) };
|
||||
return { start: startOfDay(sub(now, { days: 7 })), end: endOfDay(now) };
|
||||
case MediaFilterCoreWhen.PastMonth:
|
||||
return { start: sub(now, { months: 1 }), end: endOfDay(now) };
|
||||
return { start: startOfDay(sub(now, { months: 1 })), end: endOfDay(now) };
|
||||
case MediaFilterCoreWhen.Custom:
|
||||
if (value.custom) {
|
||||
return value.custom;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -66,22 +78,44 @@ export class FrigateCardMediaFilter extends LitElement {
|
||||
protected _convertFavoriteToBoolean(
|
||||
value?: MediaFilterCoreFavoriteSelection,
|
||||
): boolean | null {
|
||||
if (!value || value === MediaFilterCoreFavoriteSelection.All) {
|
||||
if (!value) {
|
||||
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 }),
|
||||
protected async _mediaFilterHandler(
|
||||
ev: CustomEvent<MediaFilterCoreSelection>,
|
||||
): Promise<void> {
|
||||
if (!this.cameras || !this.cameraManager || !this.hass || !this.view) {
|
||||
return;
|
||||
}
|
||||
const mediaFilter = ev.detail;
|
||||
const convertedTime = this._convertWhenToDateRange(mediaFilter.when);
|
||||
const convertedFavorite = this._convertFavoriteToBoolean(mediaFilter.favorite);
|
||||
|
||||
const query: EventQuery = {
|
||||
type: QueryType.Event,
|
||||
cameraIDs: mediaFilter.cameraIDs ?? new Set(this.cameras.keys()),
|
||||
...(mediaFilter.what && { what: mediaFilter.what }),
|
||||
...(mediaFilter.where && { where: mediaFilter.where }),
|
||||
...(convertedFavorite !== null && { favorite: convertedFavorite }),
|
||||
...(convertedTime && { start: convertedTime.start, end: convertedTime.end }),
|
||||
...(this.mediaLimit && { limit: this.mediaLimit }),
|
||||
};
|
||||
// TODO: remove.
|
||||
console.debug('Received media filter choices:', details);
|
||||
|
||||
(
|
||||
await createViewForEvents(
|
||||
this,
|
||||
this.hass,
|
||||
this.cameraManager,
|
||||
this.cameras,
|
||||
this.view,
|
||||
{
|
||||
query: new EventMediaQueries([query]),
|
||||
},
|
||||
)
|
||||
)?.dispatchChangeEvent(this);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
|
||||
+24
-12
@@ -1,6 +1,5 @@
|
||||
import format from 'date-fns/format';
|
||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
import { CSSResult, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { CSSResult, html, LitElement, PropertyValues, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
@@ -12,9 +11,9 @@ import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
import { errorToConsole, getDurationString, prettifyTitle } from '../utils/basic.js';
|
||||
import { getCameraTitle } from '../utils/camera.js';
|
||||
import { renderTask } from '../utils/task.js';
|
||||
import { createFetchThumbnailTask } from '../utils/thumbnail.js';
|
||||
import { createFetchThumbnailTask, FetchThumbnailTaskArgs } from '../utils/thumbnail.js';
|
||||
import { View } from '../view/view.js';
|
||||
import { TaskStatus } from '@lit-labs/task';
|
||||
import { Task, TaskStatus } from '@lit-labs/task';
|
||||
|
||||
import type { CameraConfig, ExtendedHomeAssistant } from '../types.js';
|
||||
import { EventViewMedia, RecordingViewMedia, ViewMedia } from '../view/media.js';
|
||||
@@ -32,12 +31,7 @@ export class FrigateCardThumbnailFeatureEvent extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: ExtendedHomeAssistant;
|
||||
|
||||
protected _embedThumbnailTask = createFetchThumbnailTask(
|
||||
this,
|
||||
() => this.hass,
|
||||
() => this.thumbnail,
|
||||
false,
|
||||
);
|
||||
protected _embedThumbnailTask?: Task<FetchThumbnailTaskArgs, string | null>;
|
||||
|
||||
// Only load thumbnails on view in case there is a very large number of them.
|
||||
protected _intersectionObserver: IntersectionObserver;
|
||||
@@ -65,20 +59,38 @@ export class FrigateCardThumbnailFeatureEvent extends LitElement {
|
||||
this._intersectionObserver.disconnect();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('thumbnail')) {
|
||||
this._embedThumbnailTask = createFetchThumbnailTask(
|
||||
this,
|
||||
() => this.hass,
|
||||
() => this.thumbnail,
|
||||
false,
|
||||
);
|
||||
// Reset the observer so the initial intersection handler call will set
|
||||
// the visibility correctly.
|
||||
this._intersectionObserver.unobserve(this);
|
||||
this._intersectionObserver.observe(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the live view intersects with the viewport.
|
||||
* @param entries The IntersectionObserverEntry entries (should be only 1).
|
||||
*/
|
||||
protected _intersectionHandler(entries: IntersectionObserverEntry[]): void {
|
||||
if (
|
||||
this._embedThumbnailTask.status === TaskStatus.INITIAL &&
|
||||
this._embedThumbnailTask?.status === TaskStatus.INITIAL &&
|
||||
entries.some((entry) => entry.isIntersecting)
|
||||
) {
|
||||
this._embedThumbnailTask.run();
|
||||
this._embedThumbnailTask?.run();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._embedThumbnailTask) {
|
||||
return;
|
||||
}
|
||||
const imageOff = html`<ha-icon
|
||||
icon="mdi:image-off"
|
||||
title=${localize('thumbnail.no_thumbnail')}
|
||||
|
||||
Reference in New Issue
Block a user