Add initial media filtering for the gallery.
This commit is contained in:
@@ -268,13 +268,14 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
const nativeQuery: NativeFrigateEventQuery = {
|
const nativeQuery: NativeFrigateEventQuery = {
|
||||||
instance_id: instanceID,
|
instance_id: instanceID,
|
||||||
cameras: Array.from(this._getFrigateCameraNamesForCameraIDs(cameras, cameraIDs)),
|
cameras: Array.from(this._getFrigateCameraNamesForCameraIDs(cameras, cameraIDs)),
|
||||||
...(query.what && { label: Array.from(query.what) }),
|
...(query.what && { labels: Array.from(query.what) }),
|
||||||
...(query.where && { zone: Array.from(query.where) }),
|
...(query.where && { zones: Array.from(query.where) }),
|
||||||
...(query.end && { before: Math.floor(query.end.getTime() / 1000) }),
|
...(query.end && { before: Math.floor(query.end.getTime() / 1000) }),
|
||||||
...(query.start && { after: Math.floor(query.start.getTime() / 1000) }),
|
...(query.start && { after: Math.floor(query.start.getTime() / 1000) }),
|
||||||
...(query.limit && { limit: query.limit }),
|
...(query.limit && { limit: query.limit }),
|
||||||
...(query.hasClip && { has_clip: query.hasClip }),
|
...(query.hasClip && { has_clip: query.hasClip }),
|
||||||
...(query.hasSnapshot && { has_snapshot: query.hasSnapshot }),
|
...(query.hasSnapshot && { has_snapshot: query.hasSnapshot }),
|
||||||
|
...(query.favorite && { favorites: query.favorite }),
|
||||||
limit: query?.limit ?? CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT,
|
limit: query?.limit ?? CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export interface NativeFrigateEventQuery {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
has_clip?: boolean;
|
has_clip?: boolean;
|
||||||
has_snapshot?: boolean;
|
has_snapshot?: boolean;
|
||||||
|
favorites?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -87,31 +87,31 @@ export class CameraManager {
|
|||||||
|
|
||||||
public generateDefaultEventQueries(
|
public generateDefaultEventQueries(
|
||||||
cameraIDs: string | Set<string>,
|
cameraIDs: string | Set<string>,
|
||||||
partialQuery: PartialEventQuery,
|
partialQuery?: PartialEventQuery,
|
||||||
): EventQuery[] | null {
|
): EventQuery[] | null {
|
||||||
return this._generateDefaultQueries(cameraIDs, {
|
return this._generateDefaultQueries(cameraIDs, {
|
||||||
...partialQuery,
|
|
||||||
type: QueryType.Event,
|
type: QueryType.Event,
|
||||||
|
...partialQuery,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultRecordingQueries(
|
public generateDefaultRecordingQueries(
|
||||||
cameraIDs: string | Set<string>,
|
cameraIDs: string | Set<string>,
|
||||||
partialQuery: PartialRecordingQuery,
|
partialQuery?: PartialRecordingQuery,
|
||||||
): RecordingQuery[] | null {
|
): RecordingQuery[] | null {
|
||||||
return this._generateDefaultQueries(cameraIDs, {
|
return this._generateDefaultQueries(cameraIDs, {
|
||||||
...partialQuery,
|
|
||||||
type: QueryType.Recording,
|
type: QueryType.Recording,
|
||||||
|
...partialQuery,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultRecordingSegmentsQueries(
|
public generateDefaultRecordingSegmentsQueries(
|
||||||
cameraIDs: string | Set<string>,
|
cameraIDs: string | Set<string>,
|
||||||
partialQuery: PartialRecordingSegmentsQuery,
|
partialQuery?: PartialRecordingSegmentsQuery,
|
||||||
): RecordingSegmentsQuery[] | null {
|
): RecordingSegmentsQuery[] | null {
|
||||||
return this._generateDefaultQueries(cameraIDs, {
|
return this._generateDefaultQueries(cameraIDs, {
|
||||||
...partialQuery,
|
|
||||||
type: QueryType.RecordingSegments,
|
type: QueryType.RecordingSegments,
|
||||||
|
...partialQuery,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ export interface EventQuery extends MediaQuery {
|
|||||||
|
|
||||||
// Frigate equivalent: zone
|
// Frigate equivalent: zone
|
||||||
where?: Set<string>;
|
where?: Set<string>;
|
||||||
|
|
||||||
|
favorite?: boolean;
|
||||||
}
|
}
|
||||||
export type PartialEventQuery = Partial<EventQuery>;
|
export type PartialEventQuery = Partial<EventQuery>;
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameras=${this.cameras}
|
.cameras=${this.cameras}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
|
.view=${this.view}
|
||||||
|
.mediaLimit=${GALLERY_MEDIA_CHUNK_SIZE}
|
||||||
slot="right"
|
slot="right"
|
||||||
>
|
>
|
||||||
</frigate-card-media-filter>
|
</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 { customElement, property } from 'lit/decorators.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import { DateRange } from '../camera/range';
|
import { DateRange } from '../camera/range';
|
||||||
@@ -13,9 +20,9 @@ export interface ValueLabel<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MediaFilterCoreSelection {
|
export interface MediaFilterCoreSelection {
|
||||||
camera?: string[];
|
cameraIDs?: Set<string>;
|
||||||
what?: string[];
|
what?: Set<string>;
|
||||||
where?: string[];
|
where?: Set<string>;
|
||||||
when?: MediaFilterCoreWhenSelection;
|
when?: MediaFilterCoreWhenSelection;
|
||||||
favorite?: MediaFilterCoreFavoriteSelection;
|
favorite?: MediaFilterCoreFavoriteSelection;
|
||||||
}
|
}
|
||||||
@@ -25,13 +32,11 @@ type FilterElement<T> = HTMLElement & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export enum MediaFilterCoreFavoriteSelection {
|
export enum MediaFilterCoreFavoriteSelection {
|
||||||
All = 'all',
|
|
||||||
Favorite = 'favorite',
|
Favorite = 'favorite',
|
||||||
NotFavorite = 'not-favorite',
|
NotFavorite = 'not-favorite',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MediaFilterCoreWhen {
|
export enum MediaFilterCoreWhen {
|
||||||
All = 'all',
|
|
||||||
Today = 'today',
|
Today = 'today',
|
||||||
Yesterday = 'yesterday',
|
Yesterday = 'yesterday',
|
||||||
PastWeek = 'past-week',
|
PastWeek = 'past-week',
|
||||||
@@ -50,10 +55,10 @@ export class FrigateCardMediaFilterCore extends LitElement {
|
|||||||
public hass?: ExtendedHomeAssistant;
|
public hass?: ExtendedHomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public whenOptions?: ValueLabel<MediaFilterCoreWhenSelection>[];
|
public cameraOptions?: ValueLabel<string>[];
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraOptions?: ValueLabel<string>[];
|
public whenOptions?: ValueLabel<MediaFilterCoreWhenSelection>[];
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public whatOptions?: ValueLabel<string>[];
|
public whatOptions?: ValueLabel<string>[];
|
||||||
@@ -61,54 +66,27 @@ export class FrigateCardMediaFilterCore extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public whereOptions?: ValueLabel<string>[];
|
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 _refCamera: Ref<FilterElement<string>> = createRef();
|
||||||
|
protected _refWhen: Ref<FilterElement<MediaFilterCoreWhenSelection>> = createRef();
|
||||||
protected _refWhat: Ref<FilterElement<string>> = createRef();
|
protected _refWhat: Ref<FilterElement<string>> = createRef();
|
||||||
protected _refWhere: Ref<FilterElement<string>> = createRef();
|
protected _refWhere: Ref<FilterElement<string>> = createRef();
|
||||||
protected _refFavorite: Ref<FilterElement<MediaFilterCoreFavoriteSelection>> =
|
protected _refFavorite: Ref<FilterElement<MediaFilterCoreFavoriteSelection>> =
|
||||||
createRef();
|
createRef();
|
||||||
|
|
||||||
protected _valueChangedHandler(ev: CustomEvent<{ value: unknown }>): void {
|
constructor() {
|
||||||
// Handler is called on initial load -- skip it.
|
super();
|
||||||
if (!ev.detail.value) {
|
this._favoriteOptions = [
|
||||||
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,
|
value: undefined,
|
||||||
label: localize('media_filter.all'),
|
label: localize('media_filter.all'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -120,39 +98,100 @@ export class FrigateCardMediaFilterCore extends LitElement {
|
|||||||
label: localize('media_filter.not_favorite'),
|
label: localize('media_filter.not_favorite'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Time based options are not pre-computed here to ensure relative dates
|
protected _valueChangedHandler(ev: CustomEvent<{ value: unknown }>): void {
|
||||||
// (e.g. 'today') are always calculated when activated not when rendered.
|
// Handler is called on initial load -- skip it.
|
||||||
const whenOptions = [
|
if (!ev.detail.value) {
|
||||||
{
|
return;
|
||||||
value: { selection: MediaFilterCoreWhen.All },
|
}
|
||||||
label: localize('media_filter.all'),
|
const values: MediaFilterCoreSelection = {
|
||||||
},
|
...(this._refCamera.value &&
|
||||||
{
|
this._refCamera.value.selectedItem?.value && {
|
||||||
value: { selection: MediaFilterCoreWhen.Today },
|
cameraIDs: new Set([this._refCamera.value.selectedItem.value]),
|
||||||
label: localize('media_filter.today'),
|
}),
|
||||||
},
|
...(this._refWhen.value &&
|
||||||
{
|
this._refWhen.value.selectedItem?.value && {
|
||||||
value: { selection: MediaFilterCoreWhen.Yesterday },
|
when: this._refWhen.value.selectedItem?.value,
|
||||||
label: localize('media_filter.yesterday'),
|
}),
|
||||||
},
|
...(this._refWhat.value &&
|
||||||
{
|
this._refWhat.value.selectedItem?.value && {
|
||||||
value: { selection: MediaFilterCoreWhen.PastWeek },
|
what: new Set([this._refWhat.value.selectedItem.value]),
|
||||||
label: localize('media_filter.past_week'),
|
}),
|
||||||
},
|
...(this._refWhere.value &&
|
||||||
{
|
this._refWhere.value.selectedItem?.value && {
|
||||||
value: { selection: MediaFilterCoreWhen.PastMonth },
|
where: new Set([this._refWhere.value.selectedItem?.value]),
|
||||||
label: localize('media_filter.past_month'),
|
}),
|
||||||
},
|
...(this._refFavorite.value &&
|
||||||
...(this.whenOptions ?? []),
|
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`
|
return html`
|
||||||
<ha-combo-box
|
<ha-combo-box
|
||||||
${ref(this._refWhen)}
|
${ref(this._refWhen)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.label=${localize('media_filter.when')}
|
.label=${localize('media_filter.when')}
|
||||||
.items=${whenOptions}
|
.items=${this._whenOptions}
|
||||||
.allowCustomValue=${false}
|
.allowCustomValue=${false}
|
||||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
@value-changed=${this._valueChangedHandler.bind(this)}
|
||||||
></ha-combo-box>
|
></ha-combo-box>
|
||||||
@@ -161,7 +200,7 @@ export class FrigateCardMediaFilterCore extends LitElement {
|
|||||||
${ref(this._refCamera)}
|
${ref(this._refCamera)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.label=${localize('media_filter.camera')}
|
.label=${localize('media_filter.camera')}
|
||||||
.items=${this.cameraOptions}
|
.items=${this._cameraOptions}
|
||||||
.allowCustomValue=${false}
|
.allowCustomValue=${false}
|
||||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
@value-changed=${this._valueChangedHandler.bind(this)}
|
||||||
></ha-combo-box>`
|
></ha-combo-box>`
|
||||||
@@ -171,7 +210,7 @@ export class FrigateCardMediaFilterCore extends LitElement {
|
|||||||
${ref(this._refWhat)}
|
${ref(this._refWhat)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.label=${localize('media_filter.what')}
|
.label=${localize('media_filter.what')}
|
||||||
.items=${this.whatOptions}
|
.items="${this._whatOptions}"
|
||||||
.allowCustomValue=${false}
|
.allowCustomValue=${false}
|
||||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
@value-changed=${this._valueChangedHandler.bind(this)}
|
||||||
></ha-combo-box>`
|
></ha-combo-box>`
|
||||||
@@ -181,7 +220,7 @@ export class FrigateCardMediaFilterCore extends LitElement {
|
|||||||
${ref(this._refWhere)}
|
${ref(this._refWhere)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.label=${localize('media_filter.where')}
|
.label=${localize('media_filter.where')}
|
||||||
.items=${this.whereOptions}
|
.items=${this._whereOptions}
|
||||||
.allowCustomValue=${false}
|
.allowCustomValue=${false}
|
||||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
@value-changed=${this._valueChangedHandler.bind(this)}
|
||||||
></ha-combo-box>`
|
></ha-combo-box>`
|
||||||
@@ -190,7 +229,7 @@ export class FrigateCardMediaFilterCore extends LitElement {
|
|||||||
${ref(this._refFavorite)}
|
${ref(this._refFavorite)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.label=${localize('media_filter.favorite')}
|
.label=${localize('media_filter.favorite')}
|
||||||
.items=${favoriteOptions}
|
.items=${this._favoriteOptions}
|
||||||
.allowCustomValue=${false}
|
.allowCustomValue=${false}
|
||||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
@value-changed=${this._valueChangedHandler.bind(this)}
|
||||||
></ha-combo-box>
|
></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 endOfDay from 'date-fns/endOfDay';
|
||||||
import endOfYesterday from 'date-fns/endOfYesterday';
|
import endOfYesterday from 'date-fns/endOfYesterday';
|
||||||
import endOfToday from 'date-fns/esm/endOfToday';
|
import endOfToday from 'date-fns/esm/endOfToday';
|
||||||
import startOfToday from 'date-fns/esm/startOfToday';
|
import startOfToday from 'date-fns/esm/startOfToday';
|
||||||
|
import startOfDay from 'date-fns/startOfDay';
|
||||||
import startOfYesterday from 'date-fns/startOfYesterday';
|
import startOfYesterday from 'date-fns/startOfYesterday';
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
@@ -17,6 +18,7 @@ import { CameraManager } from '../camera/manager';
|
|||||||
import { DateRange } from '../camera/range';
|
import { DateRange } from '../camera/range';
|
||||||
import { CameraConfig, ExtendedHomeAssistant } from '../types';
|
import { CameraConfig, ExtendedHomeAssistant } from '../types';
|
||||||
import { getCameraTitle } from '../utils/camera';
|
import { getCameraTitle } from '../utils/camera';
|
||||||
|
import { View } from '../view/view';
|
||||||
import {
|
import {
|
||||||
MediaFilterCoreFavoriteSelection,
|
MediaFilterCoreFavoriteSelection,
|
||||||
MediaFilterCoreSelection,
|
MediaFilterCoreSelection,
|
||||||
@@ -26,6 +28,9 @@ import {
|
|||||||
} from './media-filter-core';
|
} from './media-filter-core';
|
||||||
import './surround.js';
|
import './surround.js';
|
||||||
import './timeline-core.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')
|
@customElement('frigate-card-media-filter')
|
||||||
export class FrigateCardMediaFilter extends LitElement {
|
export class FrigateCardMediaFilter extends LitElement {
|
||||||
@@ -38,6 +43,12 @@ export class FrigateCardMediaFilter extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraManager?: CameraManager;
|
public cameraManager?: CameraManager;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public view?: View;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public mediaLimit?: number;
|
||||||
|
|
||||||
protected _cameraOptions: ValueLabel<string>[] = [];
|
protected _cameraOptions: ValueLabel<string>[] = [];
|
||||||
|
|
||||||
protected _convertWhenToDateRange(
|
protected _convertWhenToDateRange(
|
||||||
@@ -46,9 +57,6 @@ export class FrigateCardMediaFilter extends LitElement {
|
|||||||
if (!value) {
|
if (!value) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (value.selection === MediaFilterCoreWhen.Custom && value.custom) {
|
|
||||||
return value.custom;
|
|
||||||
}
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
switch (value.selection) {
|
switch (value.selection) {
|
||||||
case MediaFilterCoreWhen.Today:
|
case MediaFilterCoreWhen.Today:
|
||||||
@@ -56,9 +64,13 @@ export class FrigateCardMediaFilter extends LitElement {
|
|||||||
case MediaFilterCoreWhen.Yesterday:
|
case MediaFilterCoreWhen.Yesterday:
|
||||||
return { start: startOfYesterday(), end: endOfYesterday() };
|
return { start: startOfYesterday(), end: endOfYesterday() };
|
||||||
case MediaFilterCoreWhen.PastWeek:
|
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:
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -66,22 +78,44 @@ export class FrigateCardMediaFilter extends LitElement {
|
|||||||
protected _convertFavoriteToBoolean(
|
protected _convertFavoriteToBoolean(
|
||||||
value?: MediaFilterCoreFavoriteSelection,
|
value?: MediaFilterCoreFavoriteSelection,
|
||||||
): boolean | null {
|
): boolean | null {
|
||||||
if (!value || value === MediaFilterCoreFavoriteSelection.All) {
|
if (!value) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return value === MediaFilterCoreFavoriteSelection.Favorite;
|
return value === MediaFilterCoreFavoriteSelection.Favorite;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _mediaFilterHandler(ev: CustomEvent<MediaFilterCoreSelection>): void {
|
protected async _mediaFilterHandler(
|
||||||
const convertedTime = this._convertWhenToDateRange(ev.detail.when);
|
ev: CustomEvent<MediaFilterCoreSelection>,
|
||||||
const convertedFavorite = this._convertFavoriteToBoolean(ev.detail.favorite);
|
): Promise<void> {
|
||||||
const details = {
|
if (!this.cameras || !this.cameraManager || !this.hass || !this.view) {
|
||||||
...ev.detail,
|
return;
|
||||||
...(convertedTime && { when: convertedTime }),
|
}
|
||||||
...(convertedFavorite && { favorite: convertedFavorite }),
|
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 {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
|
|||||||
+24
-12
@@ -1,6 +1,5 @@
|
|||||||
import format from 'date-fns/format';
|
import format from 'date-fns/format';
|
||||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
import { CSSResult, html, LitElement, PropertyValues, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { CSSResult, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { localize } from '../localize/localize.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 { errorToConsole, getDurationString, prettifyTitle } from '../utils/basic.js';
|
||||||
import { getCameraTitle } from '../utils/camera.js';
|
import { getCameraTitle } from '../utils/camera.js';
|
||||||
import { renderTask } from '../utils/task.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 { 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 type { CameraConfig, ExtendedHomeAssistant } from '../types.js';
|
||||||
import { EventViewMedia, RecordingViewMedia, ViewMedia } from '../view/media.js';
|
import { EventViewMedia, RecordingViewMedia, ViewMedia } from '../view/media.js';
|
||||||
@@ -32,12 +31,7 @@ export class FrigateCardThumbnailFeatureEvent extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hass?: ExtendedHomeAssistant;
|
public hass?: ExtendedHomeAssistant;
|
||||||
|
|
||||||
protected _embedThumbnailTask = createFetchThumbnailTask(
|
protected _embedThumbnailTask?: Task<FetchThumbnailTaskArgs, string | null>;
|
||||||
this,
|
|
||||||
() => this.hass,
|
|
||||||
() => this.thumbnail,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Only load thumbnails on view in case there is a very large number of them.
|
// Only load thumbnails on view in case there is a very large number of them.
|
||||||
protected _intersectionObserver: IntersectionObserver;
|
protected _intersectionObserver: IntersectionObserver;
|
||||||
@@ -65,20 +59,38 @@ export class FrigateCardThumbnailFeatureEvent extends LitElement {
|
|||||||
this._intersectionObserver.disconnect();
|
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.
|
* Called when the live view intersects with the viewport.
|
||||||
* @param entries The IntersectionObserverEntry entries (should be only 1).
|
* @param entries The IntersectionObserverEntry entries (should be only 1).
|
||||||
*/
|
*/
|
||||||
protected _intersectionHandler(entries: IntersectionObserverEntry[]): void {
|
protected _intersectionHandler(entries: IntersectionObserverEntry[]): void {
|
||||||
if (
|
if (
|
||||||
this._embedThumbnailTask.status === TaskStatus.INITIAL &&
|
this._embedThumbnailTask?.status === TaskStatus.INITIAL &&
|
||||||
entries.some((entry) => entry.isIntersecting)
|
entries.some((entry) => entry.isIntersecting)
|
||||||
) {
|
) {
|
||||||
this._embedThumbnailTask.run();
|
this._embedThumbnailTask?.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this._embedThumbnailTask) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const imageOff = html`<ha-icon
|
const imageOff = html`<ha-icon
|
||||||
icon="mdi:image-off"
|
icon="mdi:image-off"
|
||||||
title=${localize('thumbnail.no_thumbnail')}
|
title=${localize('thumbnail.no_thumbnail')}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const fetchThumbnail = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
type FetchThumbnailTaskArgs = [boolean, string | undefined];
|
export type FetchThumbnailTaskArgs = [boolean, string | undefined];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a Lit task to fetch a thumbnail.
|
* Create a Lit task to fetch a thumbnail.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
// - TODO: Handle all TODOs in media filter and media filter core.
|
// - TODO: Handle all TODOs in media filter and media filter core.
|
||||||
// - TODO: Make media filter slot choice configurable (gallery.ts).
|
// - TODO: Make media filter slot choice configurable (gallery.ts).
|
||||||
// - TODO: Filter panel expands from right can occasionally 'stick' open.
|
// - TODO: Filter panel expands from right can occasionally 'stick' open.
|
||||||
|
// - TODO: Media type selection, i.e. events or recordings.
|
||||||
|
|
||||||
// Hard:
|
// Hard:
|
||||||
// - TODO: Implement gallery.
|
// - TODO: Implement gallery.
|
||||||
|
|||||||
Reference in New Issue
Block a user