Implement media filtering.
This commit is contained in:
@@ -17,6 +17,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cycjimmy/jsmpeg-player": "^6.0.4",
|
"@cycjimmy/jsmpeg-player": "^6.0.4",
|
||||||
"@egjs/hammerjs": "^2.0.17",
|
"@egjs/hammerjs": "^2.0.17",
|
||||||
|
"@graphiteds/core": "^1.9.6",
|
||||||
|
"@lit-labs/scoped-registry-mixin": "^1.0.1",
|
||||||
"@lit-labs/task": "^1.1.3",
|
"@lit-labs/task": "^1.1.3",
|
||||||
"@types/bluebird": "^3.5.36",
|
"@types/bluebird": "^3.5.36",
|
||||||
"component-emitter": "^1.3.0",
|
"component-emitter": "^1.3.0",
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ interface LimitedDataQuery {
|
|||||||
export interface MediaQuery
|
export interface MediaQuery
|
||||||
extends DataQuery,
|
extends DataQuery,
|
||||||
Partial<TimeBasedDataQuery>,
|
Partial<TimeBasedDataQuery>,
|
||||||
Partial<LimitedDataQuery> {}
|
Partial<LimitedDataQuery> {
|
||||||
|
favorite?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface QueryResults {
|
export interface QueryResults {
|
||||||
type: QueryResultsType;
|
type: QueryResultsType;
|
||||||
@@ -114,8 +116,6 @@ 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>;
|
||||||
|
|
||||||
@@ -129,8 +129,6 @@ export interface EventQueryResults extends QueryResults {
|
|||||||
|
|
||||||
export interface RecordingQuery extends MediaQuery {
|
export interface RecordingQuery extends MediaQuery {
|
||||||
type: QueryType.Recording;
|
type: QueryType.Recording;
|
||||||
|
|
||||||
favorite?: boolean;
|
|
||||||
}
|
}
|
||||||
export type PartialRecordingQuery = Partial<RecordingQuery>;
|
export type PartialRecordingQuery = Partial<RecordingQuery>;
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { EventQuery, MediaQuery, RecordingQuery } from '../camera-manager/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';
|
import './media-filter';
|
||||||
|
import "./surround-basic";
|
||||||
|
|
||||||
const GALLERY_MEDIA_CHUNK_SIZE = 100;
|
const GALLERY_MEDIA_CHUNK_SIZE = 100;
|
||||||
|
|
||||||
|
|||||||
@@ -1,313 +0,0 @@
|
|||||||
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-manager/range';
|
|
||||||
import { localize } from '../localize/localize';
|
|
||||||
import mediaFilterCoreStyle from '../scss/media-filter-core.scss';
|
|
||||||
import { ExtendedHomeAssistant } from '../types';
|
|
||||||
import { dispatchFrigateCardEvent } from '../utils/basic';
|
|
||||||
|
|
||||||
export interface ValueLabel<T> {
|
|
||||||
value?: T;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MediaFilterCoreSelection {
|
|
||||||
mediaType?: MediaFilterMediaType;
|
|
||||||
cameraIDs?: Set<string>;
|
|
||||||
what?: Set<string>;
|
|
||||||
where?: Set<string>;
|
|
||||||
when?: MediaFilterCoreWhenSelection;
|
|
||||||
favorite?: MediaFilterCoreFavoriteSelection;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilterElement<T> = HTMLElement & {
|
|
||||||
selectedItem?: ValueLabel<T>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum MediaFilterCoreFavoriteSelection {
|
|
||||||
Favorite = 'favorite',
|
|
||||||
NotFavorite = 'not-favorite',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MediaFilterCoreWhen {
|
|
||||||
Today = 'today',
|
|
||||||
Yesterday = 'yesterday',
|
|
||||||
PastWeek = 'past-week',
|
|
||||||
PastMonth = 'past-month',
|
|
||||||
Custom = 'custom',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MediaFilterMediaType {
|
|
||||||
Clips = 'clips',
|
|
||||||
Snapshots = 'snapshots',
|
|
||||||
Recordings = 'recordings',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MediaFilterCoreWhenSelection {
|
|
||||||
selection: MediaFilterCoreWhen;
|
|
||||||
custom?: DateRange;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MediaFilterControls = {
|
|
||||||
mediaType?: boolean;
|
|
||||||
when?: boolean;
|
|
||||||
camera?: boolean;
|
|
||||||
what?: boolean;
|
|
||||||
where?: boolean;
|
|
||||||
favorite?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
@customElement('frigate-card-media-filter-core')
|
|
||||||
class FrigateCardMediaFilterCore extends LitElement {
|
|
||||||
@property({ attribute: false })
|
|
||||||
public hass?: ExtendedHomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public cameraOptions?: ValueLabel<string>[];
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public whenOptions?: ValueLabel<MediaFilterCoreWhenSelection>[];
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public whatOptions?: ValueLabel<string>[];
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public whereOptions?: ValueLabel<string>[];
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public defaults?: MediaFilterCoreSelection;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public controls?: MediaFilterControls;
|
|
||||||
|
|
||||||
protected _cameraOptions?: ValueLabel<string>[];
|
|
||||||
protected _whenOptions?: ValueLabel<MediaFilterCoreWhenSelection>[];
|
|
||||||
protected _whatOptions?: ValueLabel<string>[];
|
|
||||||
protected _whereOptions?: ValueLabel<string>[];
|
|
||||||
protected _favoriteOptions: ValueLabel<MediaFilterCoreFavoriteSelection>[];
|
|
||||||
protected _mediaTypeOptions: ValueLabel<MediaFilterMediaType>[];
|
|
||||||
|
|
||||||
protected _refMediaType: Ref<FilterElement<MediaFilterMediaType>> = createRef();
|
|
||||||
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();
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this._favoriteOptions = [
|
|
||||||
{
|
|
||||||
value: undefined,
|
|
||||||
label: localize('media_filter.all'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: MediaFilterCoreFavoriteSelection.Favorite,
|
|
||||||
label: localize('media_filter.favorite'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: MediaFilterCoreFavoriteSelection.NotFavorite,
|
|
||||||
label: localize('media_filter.not_favorite'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
this._mediaTypeOptions = [
|
|
||||||
{
|
|
||||||
value: MediaFilterMediaType.Clips,
|
|
||||||
label: localize('media_filter.media_types.clips'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: MediaFilterMediaType.Snapshots,
|
|
||||||
label: localize('media_filter.media_types.snapshots'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: MediaFilterMediaType.Recordings,
|
|
||||||
label: localize('media_filter.media_types.recordings'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _valueChangedHandler(ev: CustomEvent<{ value: unknown }>): void {
|
|
||||||
// Handler is called on initial load -- skip it.
|
|
||||||
if (!ev.detail.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const values: MediaFilterCoreSelection = {
|
|
||||||
...(this._refMediaType.value &&
|
|
||||||
this._refMediaType.value.selectedItem?.value && {
|
|
||||||
mediaType: this._refMediaType.value.selectedItem?.value,
|
|
||||||
}),
|
|
||||||
...(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.whens.today'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: { selection: MediaFilterCoreWhen.Yesterday },
|
|
||||||
label: localize('media_filter.whens.yesterday'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: { selection: MediaFilterCoreWhen.PastWeek },
|
|
||||||
label: localize('media_filter.whens.past_week'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: { selection: MediaFilterCoreWhen.PastMonth },
|
|
||||||
label: localize('media_filter.whens.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` ${this.controls?.mediaType ?? true
|
|
||||||
? html` <ha-combo-box
|
|
||||||
${ref(this._refMediaType)}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.label=${localize('media_filter.media_type')}
|
|
||||||
.items=${this._mediaTypeOptions}
|
|
||||||
.allowCustomValue=${false}
|
|
||||||
.value=${this.defaults?.mediaType}
|
|
||||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
|
||||||
></ha-combo-box>`
|
|
||||||
: ''}
|
|
||||||
${this.controls?.when ?? true
|
|
||||||
? html`<ha-combo-box
|
|
||||||
${ref(this._refWhen)}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.label=${localize('media_filter.when')}
|
|
||||||
.items=${this._whenOptions}
|
|
||||||
.allowCustomValue=${false}
|
|
||||||
.value=${this.defaults?.when}
|
|
||||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
|
||||||
></ha-combo-box>`
|
|
||||||
: ''}
|
|
||||||
${(this.controls?.camera ?? true) && this.cameraOptions
|
|
||||||
? html` <ha-combo-box
|
|
||||||
${ref(this._refCamera)}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.label=${localize('media_filter.camera')}
|
|
||||||
.items=${this._cameraOptions}
|
|
||||||
.allowCustomValue=${false}
|
|
||||||
.value=${this.defaults?.cameraIDs?.size === 1
|
|
||||||
? [...this.defaults.cameraIDs][0]
|
|
||||||
: undefined}
|
|
||||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
|
||||||
></ha-combo-box>`
|
|
||||||
: ''}
|
|
||||||
${(this.controls?.what ?? true) && this.whatOptions
|
|
||||||
? html` <ha-combo-box
|
|
||||||
${ref(this._refWhat)}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.label=${localize('media_filter.what')}
|
|
||||||
.items="${this._whatOptions}"
|
|
||||||
.allowCustomValue=${false}
|
|
||||||
.value=${this.defaults?.what?.size === 1
|
|
||||||
? [...this.defaults.what][0]
|
|
||||||
: undefined}
|
|
||||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
|
||||||
></ha-combo-box>`
|
|
||||||
: ''}
|
|
||||||
${(this.controls?.where ?? true) && this.whereOptions
|
|
||||||
? html`<ha-combo-box
|
|
||||||
${ref(this._refWhere)}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.label=${localize('media_filter.where')}
|
|
||||||
.items=${this._whereOptions}
|
|
||||||
.allowCustomValue=${false}
|
|
||||||
.value=${this.defaults?.where?.size === 1
|
|
||||||
? [...this.defaults.where][0]
|
|
||||||
: undefined}
|
|
||||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
|
||||||
></ha-combo-box>`
|
|
||||||
: ''}
|
|
||||||
${this.controls?.favorite ?? true
|
|
||||||
? html`
|
|
||||||
<ha-combo-box
|
|
||||||
${ref(this._refFavorite)}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.label=${localize('media_filter.favorite')}
|
|
||||||
.items=${this._favoriteOptions}
|
|
||||||
.allowCustomValue=${false}
|
|
||||||
.value=${this.defaults?.favorite}
|
|
||||||
@value-changed=${this._valueChangedHandler.bind(this)}
|
|
||||||
></ha-combo-box>
|
|
||||||
`
|
|
||||||
: ''}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return unsafeCSS(mediaFilterCoreStyle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'frigate-card-media-filter-core': FrigateCardMediaFilterCore;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+330
-150
@@ -1,10 +1,3 @@
|
|||||||
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 {
|
import {
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
html,
|
||||||
@@ -16,38 +9,73 @@ import {
|
|||||||
unsafeCSS,
|
unsafeCSS,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { CameraManager } from '../camera-manager/manager';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import { DateRange } from '../camera-manager/range';
|
import { DateRange } from '../camera-manager/range';
|
||||||
import { CameraConfig, ExtendedHomeAssistant } from '../types';
|
import { localize } from '../localize/localize';
|
||||||
import { View } from '../view/view';
|
|
||||||
import {
|
|
||||||
MediaFilterControls,
|
|
||||||
MediaFilterCoreFavoriteSelection,
|
|
||||||
MediaFilterCoreSelection,
|
|
||||||
MediaFilterCoreWhen,
|
|
||||||
MediaFilterCoreWhenSelection,
|
|
||||||
MediaFilterMediaType,
|
|
||||||
ValueLabel,
|
|
||||||
} from './media-filter-core';
|
|
||||||
import './surround.js';
|
|
||||||
import './timeline-core.js';
|
|
||||||
import { EventQuery, MediaMetadata, QueryType, RecordingQuery } from '../camera-manager/types';
|
|
||||||
import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries';
|
|
||||||
import { createViewForEvents, createViewForRecordings } from '../utils/media-to-view.js';
|
|
||||||
import { HomeAssistant } from 'custom-card-helpers';
|
|
||||||
import { errorToConsole, prettifyTitle } from '../utils/basic';
|
|
||||||
import format from 'date-fns/format';
|
|
||||||
import parse from 'date-fns/parse';
|
|
||||||
import endOfMonth from 'date-fns/endOfMonth';
|
|
||||||
import mediaFilterStyle from '../scss/media-filter.scss';
|
import mediaFilterStyle from '../scss/media-filter.scss';
|
||||||
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
|
import { CameraConfig } from '../types';
|
||||||
|
import { createViewForEvents, createViewForRecordings } from '../utils/media-to-view.js';
|
||||||
|
import { errorToConsole, formatDate, prettifyTitle } from '../utils/basic';
|
||||||
|
import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin';
|
||||||
|
import './select';
|
||||||
|
import { FrigateCardSelect, SelectOption, SelectValues } from './select';
|
||||||
import uniqWith from 'lodash-es/uniqWith';
|
import uniqWith from 'lodash-es/uniqWith';
|
||||||
|
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 parse from 'date-fns/parse';
|
||||||
|
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
|
||||||
|
import { View } from '../view/view';
|
||||||
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import {
|
||||||
|
EventQuery,
|
||||||
|
MediaMetadata,
|
||||||
|
QueryType,
|
||||||
|
RecordingQuery,
|
||||||
|
} from '../camera-manager/types';
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import endOfMonth from 'date-fns/endOfMonth';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries';
|
||||||
|
import './select.js';
|
||||||
|
import orderBy from 'lodash-es/orderBy';
|
||||||
|
|
||||||
|
interface MediaFilterCoreDefaults {
|
||||||
|
mediaType?: MediaFilterMediaType;
|
||||||
|
cameraIDs?: string[];
|
||||||
|
what?: string[];
|
||||||
|
where?: string[];
|
||||||
|
favorite?: MediaFilterCoreFavoriteSelection;
|
||||||
|
when?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MediaFilterCoreFavoriteSelection {
|
||||||
|
Favorite = 'favorite',
|
||||||
|
NotFavorite = 'not-favorite',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MediaFilterCoreWhen {
|
||||||
|
Today = 'today',
|
||||||
|
Yesterday = 'yesterday',
|
||||||
|
PastWeek = 'past-week',
|
||||||
|
PastMonth = 'past-month',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MediaFilterMediaType {
|
||||||
|
Clips = 'clips',
|
||||||
|
Snapshots = 'snapshots',
|
||||||
|
Recordings = 'recordings',
|
||||||
|
}
|
||||||
|
|
||||||
@customElement('frigate-card-media-filter')
|
@customElement('frigate-card-media-filter')
|
||||||
export class FrigateCardMediaFilter extends LitElement {
|
class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hass?: ExtendedHomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameras?: Map<string, CameraConfig>;
|
public cameras?: Map<string, CameraConfig>;
|
||||||
@@ -61,17 +89,73 @@ export class FrigateCardMediaFilter extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public mediaLimit?: number;
|
public mediaLimit?: number;
|
||||||
|
|
||||||
protected _cameraOptions: ValueLabel<string>[] = [];
|
static elementDefinitions = {
|
||||||
|
'frigate-card-select': FrigateCardSelect,
|
||||||
|
};
|
||||||
|
|
||||||
protected _mediaMetadataController?: MediaMetadataController;
|
protected _mediaMetadataController?: MediaMetadataController;
|
||||||
|
|
||||||
protected _convertWhenToDateRange(
|
protected _mediaTypeOptions: SelectOption[];
|
||||||
value?: MediaFilterCoreWhenSelection,
|
protected _cameraOptions?: SelectOption[];
|
||||||
): DateRange | null {
|
protected _whenOptions?: SelectOption[];
|
||||||
if (!value) {
|
protected _favoriteOptions: SelectOption[];
|
||||||
|
|
||||||
|
protected _defaults: MediaFilterCoreDefaults | null = null;
|
||||||
|
|
||||||
|
protected _refMediaType: Ref<FrigateCardSelect> = createRef();
|
||||||
|
protected _refCamera: Ref<FrigateCardSelect> = createRef();
|
||||||
|
protected _refWhen: Ref<FrigateCardSelect> = createRef();
|
||||||
|
protected _refWhat: Ref<FrigateCardSelect> = createRef();
|
||||||
|
protected _refWhere: Ref<FrigateCardSelect> = createRef();
|
||||||
|
protected _refFavorite: Ref<FrigateCardSelect> = createRef();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._favoriteOptions = [
|
||||||
|
{
|
||||||
|
value: MediaFilterCoreFavoriteSelection.Favorite,
|
||||||
|
label: localize('media_filter.favorite'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MediaFilterCoreFavoriteSelection.NotFavorite,
|
||||||
|
label: localize('media_filter.not_favorite'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
this._mediaTypeOptions = [
|
||||||
|
{
|
||||||
|
value: MediaFilterMediaType.Clips,
|
||||||
|
label: localize('media_filter.media_types.clips'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MediaFilterMediaType.Snapshots,
|
||||||
|
label: localize('media_filter.media_types.snapshots'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MediaFilterMediaType.Recordings,
|
||||||
|
label: localize('media_filter.media_types.recordings'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _stringToDateRange(input: string): DateRange {
|
||||||
|
const dates = input.split(',');
|
||||||
|
return {
|
||||||
|
start: parse(dates[0], 'yyyy-MM-dd', new Date()),
|
||||||
|
end: parse(dates[1], 'yyyy-MM-dd', new Date()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _dateRangeToString(when: DateRange): string {
|
||||||
|
return `${formatDate(when.start)},${formatDate(when.end)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _getWhen(): DateRange | null {
|
||||||
|
const value = this._refWhen.value?.value;
|
||||||
|
if (!value || Array.isArray(value)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
switch (value.selection) {
|
switch (value) {
|
||||||
case MediaFilterCoreWhen.Today:
|
case MediaFilterCoreWhen.Today:
|
||||||
return { start: startOfToday(), end: endOfToday() };
|
return { start: startOfToday(), end: endOfToday() };
|
||||||
case MediaFilterCoreWhen.Yesterday:
|
case MediaFilterCoreWhen.Yesterday:
|
||||||
@@ -80,40 +164,37 @@ export class FrigateCardMediaFilter extends LitElement {
|
|||||||
return { start: startOfDay(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: startOfDay(sub(now, { months: 1 })), end: endOfDay(now) };
|
return { start: startOfDay(sub(now, { months: 1 })), end: endOfDay(now) };
|
||||||
case MediaFilterCoreWhen.Custom:
|
default:
|
||||||
if (value.custom) {
|
return this._stringToDateRange(value);
|
||||||
return value.custom;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _convertFavoriteToBoolean(
|
protected async _valueChangedHandler(
|
||||||
value?: MediaFilterCoreFavoriteSelection,
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
): boolean | null {
|
_ev: CustomEvent<{ value: unknown }>,
|
||||||
if (!value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return value === MediaFilterCoreFavoriteSelection.Favorite;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async _mediaFilterHandler(
|
|
||||||
ev: CustomEvent<MediaFilterCoreSelection>,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const mediaFilter = ev.detail;
|
if (!this.hass || !this.cameras || !this.cameraManager || !this.view) {
|
||||||
if (
|
|
||||||
!this.cameras ||
|
|
||||||
!this.cameraManager ||
|
|
||||||
!this.hass ||
|
|
||||||
!this.view ||
|
|
||||||
!mediaFilter.mediaType
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertedTime = this._convertWhenToDateRange(mediaFilter.when);
|
const getArrayValueAsSet = (val?: SelectValues): Set<string> | null => {
|
||||||
const convertedFavorite = this._convertFavoriteToBoolean(mediaFilter.favorite);
|
// The reported value may be '' if the field is clearable (i.e. the user
|
||||||
const cameraIDs = mediaFilter.cameraIDs ?? new Set(this.cameras.keys());
|
// can click 'x').
|
||||||
|
if (val && Array.isArray(val) && val.length && !val.includes('')) {
|
||||||
|
return new Set([...val]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cameraIDs =
|
||||||
|
getArrayValueAsSet(this._refCamera.value?.value) ?? new Set(this.cameras.keys());
|
||||||
|
const mediaType = this._refMediaType.value?.value as
|
||||||
|
| MediaFilterMediaType
|
||||||
|
| undefined;
|
||||||
|
const when = this._getWhen();
|
||||||
|
const favorite = this._refFavorite.value?.value
|
||||||
|
? this._refFavorite.value.value === MediaFilterCoreFavoriteSelection.Favorite
|
||||||
|
: null;
|
||||||
|
|
||||||
// A note on views:
|
// A note on views:
|
||||||
// - In the below, if the user selects a camera to view media for, the main
|
// - In the below, if the user selects a camera to view media for, the main
|
||||||
@@ -125,22 +206,27 @@ export class FrigateCardMediaFilter extends LitElement {
|
|||||||
// to 'clips' or 'snapshots' in order to ensure the right icon is shown as
|
// to 'clips' or 'snapshots' in order to ensure the right icon is shown as
|
||||||
// selected in the menu.
|
// selected in the menu.
|
||||||
if (
|
if (
|
||||||
mediaFilter.mediaType === MediaFilterMediaType.Clips ||
|
mediaType === MediaFilterMediaType.Clips ||
|
||||||
mediaFilter.mediaType === MediaFilterMediaType.Snapshots
|
mediaType === MediaFilterMediaType.Snapshots
|
||||||
) {
|
) {
|
||||||
const query: EventQuery = {
|
const where = getArrayValueAsSet(this._refWhere.value?.value);
|
||||||
type: QueryType.Event,
|
const what = getArrayValueAsSet(this._refWhat.value?.value);
|
||||||
cameraIDs: cameraIDs,
|
|
||||||
...(mediaFilter.what && { what: mediaFilter.what }),
|
const queries: EventQuery[] = [
|
||||||
...(mediaFilter.where && { where: mediaFilter.where }),
|
{
|
||||||
...(convertedFavorite !== null && { favorite: convertedFavorite }),
|
type: QueryType.Event,
|
||||||
...(convertedTime && { start: convertedTime.start, end: convertedTime.end }),
|
cameraIDs: cameraIDs,
|
||||||
...(this.mediaLimit && { limit: this.mediaLimit }),
|
...(what && { what: what }),
|
||||||
...(mediaFilter.mediaType === MediaFilterMediaType.Clips && { hasClip: true }),
|
...(where && { where: where }),
|
||||||
...(mediaFilter.mediaType === MediaFilterMediaType.Snapshots && {
|
...(favorite !== null && { favorite: favorite }),
|
||||||
hasSnapshot: true,
|
...(when && { start: when.start, end: when.end }),
|
||||||
}),
|
...(this.mediaLimit && { limit: this.mediaLimit }),
|
||||||
};
|
...(mediaType === MediaFilterMediaType.Clips && { hasClip: true }),
|
||||||
|
...(mediaType === MediaFilterMediaType.Snapshots && {
|
||||||
|
hasSnapshot: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
(
|
(
|
||||||
await createViewForEvents(
|
await createViewForEvents(
|
||||||
@@ -150,22 +236,19 @@ export class FrigateCardMediaFilter extends LitElement {
|
|||||||
this.cameras,
|
this.cameras,
|
||||||
this.view,
|
this.view,
|
||||||
{
|
{
|
||||||
query: new EventMediaQueries([query]),
|
query: new EventMediaQueries(queries),
|
||||||
|
|
||||||
// See 'A note on views' above for these two arguments.
|
// See 'A note on views' above for these two arguments.
|
||||||
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
|
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
|
||||||
targetView:
|
targetView: mediaType === MediaFilterMediaType.Clips ? 'clips' : 'snapshots',
|
||||||
mediaFilter.mediaType === MediaFilterMediaType.Clips
|
|
||||||
? 'clips'
|
|
||||||
: 'snapshots',
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)?.dispatchChangeEvent(this);
|
)?.dispatchChangeEvent(this);
|
||||||
} else if (mediaFilter.mediaType === MediaFilterMediaType.Recordings) {
|
} else if (mediaType === MediaFilterMediaType.Recordings) {
|
||||||
const query: RecordingQuery = {
|
const query: RecordingQuery = {
|
||||||
type: QueryType.Recording,
|
type: QueryType.Recording,
|
||||||
cameraIDs: cameraIDs,
|
cameraIDs: cameraIDs,
|
||||||
...(convertedTime && { start: convertedTime.start, end: convertedTime.end }),
|
...(when && { start: when.start, end: when.end }),
|
||||||
};
|
};
|
||||||
|
|
||||||
(
|
(
|
||||||
@@ -206,23 +289,74 @@ export class FrigateCardMediaFilter extends LitElement {
|
|||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Relative 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: MediaFilterCoreWhen.Today,
|
||||||
|
label: localize('media_filter.whens.today'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MediaFilterCoreWhen.Yesterday,
|
||||||
|
label: localize('media_filter.whens.yesterday'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MediaFilterCoreWhen.PastWeek,
|
||||||
|
label: localize('media_filter.whens.past_week'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: MediaFilterCoreWhen.PastMonth,
|
||||||
|
label: localize('media_filter.whens.past_month'),
|
||||||
|
},
|
||||||
|
...(this._mediaMetadataController?.whenOptions ?? []),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (changedProps.has('view')) {
|
||||||
|
const newDefaults = this._getDefaultsFromView();
|
||||||
|
if (!isEqual(newDefaults, this._defaults)) {
|
||||||
|
this._defaults = newDefaults;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _getDefaultsFromView(): MediaFilterCoreSelection | undefined {
|
protected _getDefaultsFromView(): MediaFilterCoreDefaults | null {
|
||||||
if (!this.view) {
|
const queries = this.view?.query?.getQueries();
|
||||||
return undefined;
|
if (!this.view || !queries) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mediaType: MediaFilterMediaType | undefined;
|
let mediaType: MediaFilterMediaType | undefined;
|
||||||
let cameraIDs: Set<string> | undefined;
|
let cameraIDs: string[] | undefined;
|
||||||
let what: Set<string> | undefined;
|
let what: string[] | undefined;
|
||||||
let where: Set<string> | undefined;
|
let where: string[] | undefined;
|
||||||
let favorite: boolean | undefined;
|
let favorite: MediaFilterCoreFavoriteSelection | undefined;
|
||||||
|
|
||||||
|
const cameraIDSets = uniqWith(
|
||||||
|
queries.map((query) => query.cameraIDs),
|
||||||
|
isEqual,
|
||||||
|
);
|
||||||
|
// Special note: If all cameras are selected, this is the same as no
|
||||||
|
// selector at all.
|
||||||
|
if (cameraIDSets.length === 1 && queries[0].cameraIDs.size !== this.cameras?.size) {
|
||||||
|
cameraIDs = [...queries[0].cameraIDs];
|
||||||
|
}
|
||||||
|
|
||||||
|
const favoriteValues = uniqWith(
|
||||||
|
queries.map((query) => query.favorite),
|
||||||
|
isEqual,
|
||||||
|
);
|
||||||
|
if (favoriteValues.length === 1 && queries[0].favorite !== undefined) {
|
||||||
|
favorite = queries[0].favorite
|
||||||
|
? MediaFilterCoreFavoriteSelection.Favorite
|
||||||
|
: MediaFilterCoreFavoriteSelection.NotFavorite;
|
||||||
|
}
|
||||||
|
|
||||||
if (MediaQueriesClassifier.areEventQueries(this.view.query)) {
|
if (MediaQueriesClassifier.areEventQueries(this.view.query)) {
|
||||||
const queries = this.view.query.getQueries();
|
const queries = this.view.query.getQueries();
|
||||||
if (!queries) {
|
if (!queries) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasClips = uniqWith(
|
const hasClips = uniqWith(
|
||||||
@@ -241,34 +375,22 @@ export class FrigateCardMediaFilter extends LitElement {
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cameraIDSets = uniqWith(
|
|
||||||
queries.map((query) => query.cameraIDs),
|
|
||||||
isEqual,
|
|
||||||
);
|
|
||||||
if (cameraIDSets.length === 1) {
|
|
||||||
cameraIDs = queries[0].cameraIDs;
|
|
||||||
}
|
|
||||||
const whatSets = uniqWith(
|
const whatSets = uniqWith(
|
||||||
queries.map((query) => query.what),
|
queries.map((query) => query.what),
|
||||||
isEqual,
|
isEqual,
|
||||||
);
|
);
|
||||||
if (whatSets.length === 1) {
|
if (whatSets.length === 1 && queries[0].what?.size) {
|
||||||
what = queries[0].what;
|
what = [...queries[0].what];
|
||||||
}
|
}
|
||||||
const whereSets = uniqWith(
|
const whereSets = uniqWith(
|
||||||
queries.map((query) => query.where),
|
queries.map((query) => query.where),
|
||||||
isEqual,
|
isEqual,
|
||||||
);
|
);
|
||||||
if (whereSets.length === 1) {
|
if (whereSets.length === 1 && queries[0].where?.size) {
|
||||||
where = queries[0].where;
|
where = [...queries[0].where];
|
||||||
}
|
|
||||||
const favoriteValues = uniqWith(
|
|
||||||
queries.map((query) => query.favorite),
|
|
||||||
isEqual,
|
|
||||||
);
|
|
||||||
if (favoriteValues.length === 1) {
|
|
||||||
favorite = queries[0].favorite;
|
|
||||||
}
|
}
|
||||||
|
} else if (MediaQueriesClassifier.areRecordingQueries(this.view.query)) {
|
||||||
|
mediaType = MediaFilterMediaType.Recordings;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -276,15 +398,15 @@ export class FrigateCardMediaFilter extends LitElement {
|
|||||||
...(cameraIDs && { cameraIDs: cameraIDs }),
|
...(cameraIDs && { cameraIDs: cameraIDs }),
|
||||||
...(what && { what: what }),
|
...(what && { what: what }),
|
||||||
...(where && { where: where }),
|
...(where && { where: where }),
|
||||||
...(favorite !== undefined && {
|
...(favorite !== undefined && { favorite: favorite }),
|
||||||
favorite: favorite
|
|
||||||
? MediaFilterCoreFavoriteSelection.Favorite
|
|
||||||
: MediaFilterCoreFavoriteSelection.NotFavorite,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this._mediaMetadataController) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const areEvents = !!(
|
const areEvents = !!(
|
||||||
this.view?.query && MediaQueriesClassifier.areEventQueries(this.view.query)
|
this.view?.query && MediaQueriesClassifier.areEventQueries(this.view.query)
|
||||||
);
|
);
|
||||||
@@ -294,28 +416,82 @@ export class FrigateCardMediaFilter extends LitElement {
|
|||||||
const managerCapabilities = this.cameraManager?.getCapabilities();
|
const managerCapabilities = this.cameraManager?.getCapabilities();
|
||||||
|
|
||||||
// Which media controls are shown depends on the view.
|
// Which media controls are shown depends on the view.
|
||||||
const controls: MediaFilterControls = {
|
const showFavoriteControl = areEvents
|
||||||
what: areEvents,
|
? !!managerCapabilities?.canFavoriteEvents
|
||||||
where: areEvents,
|
: areRecordings
|
||||||
favorite: areEvents
|
? !!managerCapabilities?.canFavoriteRecordings
|
||||||
? !!managerCapabilities?.canFavoriteEvents
|
: false;
|
||||||
: areRecordings
|
|
||||||
? !!managerCapabilities?.canFavoriteRecordings
|
|
||||||
: false,
|
|
||||||
};
|
|
||||||
const defaults = this._getDefaultsFromView();
|
|
||||||
|
|
||||||
return html` <frigate-card-media-filter-core
|
return html` <frigate-card-select
|
||||||
.hass=${this.hass}
|
${ref(this._refMediaType)}
|
||||||
.whenOptions=${this._mediaMetadataController?.whenOptions}
|
label=${localize('media_filter.media_type')}
|
||||||
.cameraOptions=${this._cameraOptions}
|
placeholder=${localize('media_filter.select_media_type')}
|
||||||
.whatOptions=${this._mediaMetadataController?.whatOptions}
|
.options=${this._mediaTypeOptions}
|
||||||
.whereOptions=${this._mediaMetadataController?.whereOptions}
|
.value=${this._defaults?.mediaType}
|
||||||
.controls=${controls}
|
@frigate-card:select:change=${this._valueChangedHandler.bind(this)}
|
||||||
.defaults=${defaults}
|
>
|
||||||
@frigate-card:media-filter-core:change=${this._mediaFilterHandler.bind(this)}
|
</frigate-card-select>
|
||||||
>
|
<frigate-card-select
|
||||||
</frigate-card-media-filter-core>`;
|
${ref(this._refWhen)}
|
||||||
|
.label=${localize('media_filter.when')}
|
||||||
|
placeholder=${localize('media_filter.select_when')}
|
||||||
|
.options=${this._whenOptions}
|
||||||
|
.value=${this._defaults?.when}
|
||||||
|
clearable
|
||||||
|
@frigate-card:select:change=${this._valueChangedHandler.bind(this)}
|
||||||
|
>
|
||||||
|
</frigate-card-select>
|
||||||
|
<frigate-card-select
|
||||||
|
${ref(this._refCamera)}
|
||||||
|
.label=${localize('media_filter.camera')}
|
||||||
|
placeholder=${localize('media_filter.select_camera')}
|
||||||
|
.options=${this._cameraOptions}
|
||||||
|
.value=${this._defaults?.cameraIDs}
|
||||||
|
clearable
|
||||||
|
multiple
|
||||||
|
@frigate-card:select:change=${this._valueChangedHandler.bind(this)}
|
||||||
|
>
|
||||||
|
</frigate-card-select>
|
||||||
|
${areEvents && this._mediaMetadataController.whatOptions.length
|
||||||
|
? html` <frigate-card-select
|
||||||
|
${ref(this._refWhat)}
|
||||||
|
label=${localize('media_filter.what')}
|
||||||
|
placeholder=${localize('media_filter.select_what')}
|
||||||
|
clearable
|
||||||
|
multiple
|
||||||
|
.options=${this._mediaMetadataController.whatOptions}
|
||||||
|
.value=${this._defaults?.what}
|
||||||
|
@frigate-card:select:change=${this._valueChangedHandler.bind(this)}
|
||||||
|
>
|
||||||
|
</frigate-card-select>`
|
||||||
|
: ''}
|
||||||
|
${areEvents && this._mediaMetadataController.whereOptions.length
|
||||||
|
? html` <frigate-card-select
|
||||||
|
${ref(this._refWhere)}
|
||||||
|
label=${localize('media_filter.where')}
|
||||||
|
placeholder=${localize('media_filter.select_where')}
|
||||||
|
clearable
|
||||||
|
multiple
|
||||||
|
.options=${this._mediaMetadataController.whereOptions}
|
||||||
|
.value=${this._defaults?.where}
|
||||||
|
@frigate-card:select:change=${this._valueChangedHandler.bind(this)}
|
||||||
|
>
|
||||||
|
</frigate-card-select>`
|
||||||
|
: ''}
|
||||||
|
${showFavoriteControl
|
||||||
|
? html`
|
||||||
|
<frigate-card-select
|
||||||
|
${ref(this._refFavorite)}
|
||||||
|
label=${localize('media_filter.favorite')}
|
||||||
|
placeholder=${localize('media_filter.select_favorite')}
|
||||||
|
.options=${this._favoriteOptions}
|
||||||
|
.value=${this._defaults?.favorite}
|
||||||
|
clearable
|
||||||
|
@frigate-card:select:change=${this._valueChangedHandler.bind(this)}
|
||||||
|
>
|
||||||
|
</frigate-card-select>
|
||||||
|
`
|
||||||
|
: ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
@@ -328,9 +504,9 @@ export class MediaMetadataController implements ReactiveController {
|
|||||||
protected _hass: HomeAssistant;
|
protected _hass: HomeAssistant;
|
||||||
protected _cameraManager: CameraManager;
|
protected _cameraManager: CameraManager;
|
||||||
|
|
||||||
public whenOptions: ValueLabel<MediaFilterCoreWhenSelection>[] = [];
|
public whenOptions: SelectOption[] = [];
|
||||||
public whatOptions: ValueLabel<string>[] = [];
|
public whatOptions: SelectOption[] = [];
|
||||||
public whereOptions: ValueLabel<string>[] = [];
|
public whereOptions: SelectOption[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
host: ReactiveControllerHost,
|
host: ReactiveControllerHost,
|
||||||
@@ -343,6 +519,10 @@ export class MediaMetadataController implements ReactiveController {
|
|||||||
host.addController(this);
|
host.addController(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _dateRangeToString(when: DateRange): string {
|
||||||
|
return `${formatDate(when.start)},${formatDate(when.end)}`;
|
||||||
|
}
|
||||||
|
|
||||||
async hostConnected() {
|
async hostConnected() {
|
||||||
let metadata: MediaMetadata | null;
|
let metadata: MediaMetadata | null;
|
||||||
try {
|
try {
|
||||||
@@ -375,15 +555,15 @@ export class MediaMetadataController implements ReactiveController {
|
|||||||
yearMonths.forEach((yearMonth) => {
|
yearMonths.forEach((yearMonth) => {
|
||||||
monthStarts.push(parse(yearMonth, 'yyyy-MM', new Date()));
|
monthStarts.push(parse(yearMonth, 'yyyy-MM', new Date()));
|
||||||
});
|
});
|
||||||
this.whenOptions = monthStarts
|
this.whenOptions = orderBy(monthStarts, (date) => date.getTime(), 'desc').map(
|
||||||
.sort()
|
(monthStart) => ({
|
||||||
.map((monthStart) => ({
|
|
||||||
label: format(monthStart, 'MMMM yyyy'),
|
label: format(monthStart, 'MMMM yyyy'),
|
||||||
value: {
|
value: this._dateRangeToString({
|
||||||
selection: MediaFilterCoreWhen.Custom,
|
start: monthStart,
|
||||||
custom: { start: monthStart, end: endOfMonth(monthStart) },
|
end: endOfMonth(monthStart),
|
||||||
},
|
}),
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this._host.requestUpdate();
|
this._host.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
|
import { property } from 'lit/decorators.js';
|
||||||
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
|
import selectStyle from '../scss/select.scss';
|
||||||
|
import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic';
|
||||||
|
import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin';
|
||||||
|
import { grSelectElements } from '../scoped-elements/gr-select';
|
||||||
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
import '../scoped-elements/gr-select';
|
||||||
|
|
||||||
|
export interface SelectOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectValues = string | string[];
|
||||||
|
|
||||||
|
type SelectElement = HTMLElement & {
|
||||||
|
value: SelectValues;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class FrigateCardSelect extends ScopedRegistryHost(LitElement) {
|
||||||
|
@property({ attribute: false, hasChanged: contentsChanged })
|
||||||
|
public options?: SelectOption[];
|
||||||
|
|
||||||
|
@property({ attribute: false, hasChanged: contentsChanged })
|
||||||
|
public value?: SelectValues;
|
||||||
|
|
||||||
|
@property({ attribute: true })
|
||||||
|
public label?: string;
|
||||||
|
|
||||||
|
@property({ attribute: true })
|
||||||
|
public placeholder?: string;
|
||||||
|
|
||||||
|
@property({ attribute: true, type: Boolean })
|
||||||
|
public multiple?: boolean = false;
|
||||||
|
|
||||||
|
@property({ attribute: true, type: Boolean })
|
||||||
|
public clearable?: boolean = false;
|
||||||
|
|
||||||
|
protected _previouslyReportedValue?: SelectValues;
|
||||||
|
protected _refSelect: Ref<SelectElement> = createRef();
|
||||||
|
|
||||||
|
static elementDefinitions = {
|
||||||
|
...grSelectElements,
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected _valueChangedHandler(_ev: CustomEvent<{ value: unknown }>): void {
|
||||||
|
const value: SelectValues | undefined = this._refSelect.value?.value;
|
||||||
|
// The underlying gr-select element is very sensitive and occasionally fires
|
||||||
|
// the change event even if the value has not actually changed. Prevent that
|
||||||
|
// from propagating upwards.
|
||||||
|
if (value !== undefined && !isEqual(this.value, value)) {
|
||||||
|
this.value = value;
|
||||||
|
dispatchFrigateCardEvent(this, 'select:change', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
return html` <gr-select
|
||||||
|
${ref(this._refSelect)}
|
||||||
|
label=${this.label ?? ''}
|
||||||
|
placeholder=${this.placeholder ?? ''}
|
||||||
|
size="small"
|
||||||
|
?multiple=${this.multiple}
|
||||||
|
?clearable=${this.clearable}
|
||||||
|
.value=${this.value ?? this._refSelect.value?.value ?? []}
|
||||||
|
@gr-change=${this._valueChangedHandler.bind(this)}
|
||||||
|
>
|
||||||
|
${this.options?.map(
|
||||||
|
(option) =>
|
||||||
|
html`<gr-menu-item value="${option.value ?? ''}"
|
||||||
|
>${option.label}</gr-menu-item
|
||||||
|
>`,
|
||||||
|
)}
|
||||||
|
</gr-select>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(selectStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'frigate-card-select': FrigateCardSelect;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -384,6 +384,12 @@
|
|||||||
"recordings": "Recordings"
|
"recordings": "Recordings"
|
||||||
},
|
},
|
||||||
"not_favorite": "Not Favorite",
|
"not_favorite": "Not Favorite",
|
||||||
|
"select_camera": "Select camera...",
|
||||||
|
"select_favorite": "Select favorite...",
|
||||||
|
"select_media_type": "Select media type...",
|
||||||
|
"select_what": "Select what...",
|
||||||
|
"select_when": "Select when...",
|
||||||
|
"select_where": "Select where...",
|
||||||
"what": "What",
|
"what": "What",
|
||||||
"when": "When",
|
"when": "When",
|
||||||
"whens": {
|
"whens": {
|
||||||
|
|||||||
@@ -355,6 +355,12 @@
|
|||||||
"recordings": ""
|
"recordings": ""
|
||||||
},
|
},
|
||||||
"not_favorite": "",
|
"not_favorite": "",
|
||||||
|
"select_camera": "",
|
||||||
|
"select_favorite": "",
|
||||||
|
"select_media_type": "",
|
||||||
|
"select_what": "",
|
||||||
|
"select_when": "",
|
||||||
|
"select_where": "",
|
||||||
"what": "",
|
"what": "",
|
||||||
"when": "",
|
"when": "",
|
||||||
"whens": {
|
"whens": {
|
||||||
|
|||||||
@@ -355,6 +355,12 @@
|
|||||||
"recordings": ""
|
"recordings": ""
|
||||||
},
|
},
|
||||||
"not_favorite": "",
|
"not_favorite": "",
|
||||||
|
"select_camera": "",
|
||||||
|
"select_favorite": "",
|
||||||
|
"select_media_type": "",
|
||||||
|
"select_what": "",
|
||||||
|
"select_when": "",
|
||||||
|
"select_where": "",
|
||||||
"what": "",
|
"what": "",
|
||||||
"when": "",
|
"when": "",
|
||||||
"whens": {
|
"whens": {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { GrSelect } from '@graphiteds/core/components/gr-select';
|
||||||
|
import { GrMenuItem } from '@graphiteds/core/components/gr-menu-item';
|
||||||
|
|
||||||
|
// It was difficult to find a multi-select web component that matches these criteria:
|
||||||
|
// - Open source.
|
||||||
|
// - Supports being in a ScopedRegistry out of the box (i.e. does not auto-register with customElements).
|
||||||
|
// - Looks attractive / compatible with mostly Material elements.
|
||||||
|
// - Styleable
|
||||||
|
// - Does not bloat output size considerably.
|
||||||
|
|
||||||
|
// Web components evaluated (https://open-wc.org/guides/community/component-libraries/):
|
||||||
|
// - Material: No multiselect component.
|
||||||
|
// - Freshwords/@crayon: Considerable bloat in output due to i18n translations
|
||||||
|
// that are used by _other_ components.
|
||||||
|
// - Carbon Design System: Workable, but less moderm / Material-like.
|
||||||
|
// - UI5: Auto-registers globally.
|
||||||
|
// - Vaadin: Auto-registers globally.
|
||||||
|
// - Liquid: Not open source.
|
||||||
|
// - [Many others]: No multiselect component.
|
||||||
|
|
||||||
|
export const grSelectElements = {
|
||||||
|
'gr-select': GrSelect,
|
||||||
|
'gr-menu-item': GrMenuItem,
|
||||||
|
};
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
:host {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
// Hide scrollbar: Firefox
|
|
||||||
scrollbar-width: none;
|
|
||||||
// Hide scrollbar: IE and Edge
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
|
||||||
:host::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,20 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: flex;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
// Hide scrollbar: Firefox
|
||||||
|
scrollbar-width: none;
|
||||||
|
// Hide scrollbar: IE and Edge
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
width: 300px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
|
:host::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
@use '@graphiteds/core/css/core.css';
|
||||||
|
|
||||||
|
:host {
|
||||||
|
// The graphite css (above) loads variables into :root, which is lost in the
|
||||||
|
// shadow DOM, so copy them into the host.
|
||||||
|
@extend :root;
|
||||||
|
}
|
||||||
|
|
||||||
|
gr-select {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
// Gallery:
|
// Gallery:
|
||||||
// - TODO: Filter panel expands from right can occasionally 'stick' open.
|
// - TODO: View a recording from the gallery => timeline should span the hour.
|
||||||
|
|
||||||
import { ViewContext } from 'view';
|
import { ViewContext } from 'view';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -270,6 +270,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@cycjimmy/jsmpeg-player/-/jsmpeg-player-6.0.4.tgz#2ad00118a48bef30db0115ba873b335fa1559ae7"
|
resolved "https://registry.yarnpkg.com/@cycjimmy/jsmpeg-player/-/jsmpeg-player-6.0.4.tgz#2ad00118a48bef30db0115ba873b335fa1559ae7"
|
||||||
integrity sha512-w3E1nwSDKPJaBD3bq/aMvR3hgUJpyCrqX2jJbQAjE9g4xk9bRkeAIH1h1mBtdRFMpTzUlE3GX8r44lXEH0dVVQ==
|
integrity sha512-w3E1nwSDKPJaBD3bq/aMvR3hgUJpyCrqX2jJbQAjE9g4xk9bRkeAIH1h1mBtdRFMpTzUlE3GX8r44lXEH0dVVQ==
|
||||||
|
|
||||||
|
"@duetds/date-picker@^1.4.0":
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@duetds/date-picker/-/date-picker-1.4.0.tgz#4bdbc15aa4b6307b8b156d8d9c5fdf67f6b6783b"
|
||||||
|
integrity sha512-jx4oSIrZAVsXYLyQGXbEmuo0gEXAqrNIGp3rG8L03XSxFFFca/bTpCj5v6yd48vVWOtAXD8y04S/+flOimra0A==
|
||||||
|
dependencies:
|
||||||
|
"@stencil/core" "^2.3.0"
|
||||||
|
|
||||||
"@egjs/hammerjs@^2.0.17":
|
"@egjs/hammerjs@^2.0.17":
|
||||||
version "2.0.17"
|
version "2.0.17"
|
||||||
resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124"
|
resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124"
|
||||||
@@ -338,6 +345,15 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
emojis-list "^3.0.0"
|
emojis-list "^3.0.0"
|
||||||
|
|
||||||
|
"@graphiteds/core@^1.9.6":
|
||||||
|
version "1.9.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@graphiteds/core/-/core-1.9.6.tgz#bbfb862a917b52d3610a09b2b8168bf9428e4007"
|
||||||
|
integrity sha512-aJ8f17wj7qPbVXH13ZR+xSztN45H0pH46I2cBV4NKnDO3xrqf88VLnOU7USGTJZ53FqgL/UGBAJfv5WjbU0dHw==
|
||||||
|
dependencies:
|
||||||
|
"@duetds/date-picker" "^1.4.0"
|
||||||
|
"@popperjs/core" "^2.11.5"
|
||||||
|
"@stencil/core" "^2.20.0"
|
||||||
|
|
||||||
"@humanwhocodes/config-array@^0.10.4":
|
"@humanwhocodes/config-array@^0.10.4":
|
||||||
version "0.10.4"
|
version "0.10.4"
|
||||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.4.tgz#01e7366e57d2ad104feea63e72248f22015c520c"
|
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.4.tgz#01e7366e57d2ad104feea63e72248f22015c520c"
|
||||||
@@ -410,6 +426,19 @@
|
|||||||
"@jridgewell/resolve-uri" "^3.0.3"
|
"@jridgewell/resolve-uri" "^3.0.3"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||||
|
|
||||||
|
"@lit-labs/scoped-registry-mixin@^1.0.1":
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@lit-labs/scoped-registry-mixin/-/scoped-registry-mixin-1.0.1.tgz#0ad266c029f1eb385711d2cd26252baadf145a5d"
|
||||||
|
integrity sha512-7aKnBKb5izcTjICO4VdeQLRYpZoB8FepJjG7TmE2oe0cU8cb4MUA4NFCjze+vbE+wP/55xv4dAMFOuJN9EG26w==
|
||||||
|
dependencies:
|
||||||
|
"@lit/reactive-element" "^1.0.0"
|
||||||
|
lit "^2.0.0"
|
||||||
|
|
||||||
|
"@lit-labs/ssr-dom-shim@^1.0.0":
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.0.0.tgz#427e19a2765681fd83411cd72c55ba80a01e0523"
|
||||||
|
integrity sha512-ic93MBXfApIFTrup4a70M/+ddD8xdt2zxxj9sRwHQzhS9ag/syqkD8JPdTXsc1gUy2K8TTirhlCqyTEM/sifNw==
|
||||||
|
|
||||||
"@lit-labs/task@^1.1.3":
|
"@lit-labs/task@^1.1.3":
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/@lit-labs/task/-/task-1.1.3.tgz#1215ae8ca746ecc895813463b81b2ce4b823098a"
|
resolved "https://registry.yarnpkg.com/@lit-labs/task/-/task-1.1.3.tgz#1215ae8ca746ecc895813463b81b2ce4b823098a"
|
||||||
@@ -417,6 +446,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@lit/reactive-element" "^1.1.0"
|
"@lit/reactive-element" "^1.1.0"
|
||||||
|
|
||||||
|
"@lit/reactive-element@^1.0.0", "@lit/reactive-element@^1.6.0":
|
||||||
|
version "1.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.6.1.tgz#0d958b6d479d0e3db5fc1132ecc4fa84be3f0b93"
|
||||||
|
integrity sha512-va15kYZr7KZNNPZdxONGQzpUr+4sxVu7V/VG7a8mRfPPXUyhEYj5RzXCQmGrlP3tAh0L3HHm5AjBMFYRqlM9SA==
|
||||||
|
dependencies:
|
||||||
|
"@lit-labs/ssr-dom-shim" "^1.0.0"
|
||||||
|
|
||||||
"@lit/reactive-element@^1.1.0", "@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.4.0":
|
"@lit/reactive-element@^1.1.0", "@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.4.0":
|
||||||
version "1.4.1"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.4.1.tgz#3f587eec5708692135bc9e94cf396130604979f3"
|
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.4.1.tgz#3f587eec5708692135bc9e94cf396130604979f3"
|
||||||
@@ -443,6 +479,11 @@
|
|||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
|
"@popperjs/core@^2.11.5":
|
||||||
|
version "2.11.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
|
||||||
|
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
|
||||||
|
|
||||||
"@rollup/plugin-babel@^5.3.1":
|
"@rollup/plugin-babel@^5.3.1":
|
||||||
version "5.3.1"
|
version "5.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
|
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
|
||||||
@@ -516,6 +557,11 @@
|
|||||||
estree-walker "^2.0.1"
|
estree-walker "^2.0.1"
|
||||||
picomatch "^2.2.2"
|
picomatch "^2.2.2"
|
||||||
|
|
||||||
|
"@stencil/core@^2.20.0", "@stencil/core@^2.3.0":
|
||||||
|
version "2.22.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@stencil/core/-/core-2.22.2.tgz#f518b6bfae85352c72cd07fcaf2f2b61e0581fd9"
|
||||||
|
integrity sha512-r+vbxsGNcBaV1VDOYW25lv4QfXTlNoIb5GpUX7rZ+cr59yqYCZC5tlV+IzX6YgHKW62ulCc9M3RYtTfHtNbNNw==
|
||||||
|
|
||||||
"@trysound/sax@0.2.0":
|
"@trysound/sax@0.2.0":
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
|
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
|
||||||
@@ -2079,6 +2125,22 @@ lit-html@^2.2.0, lit-html@^2.3.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/trusted-types" "^2.0.2"
|
"@types/trusted-types" "^2.0.2"
|
||||||
|
|
||||||
|
lit-html@^2.6.0:
|
||||||
|
version "2.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.6.1.tgz#eb29f0b0c2ab54ea77379db11fc011b0c71f1cda"
|
||||||
|
integrity sha512-Z3iw+E+3KKFn9t2YKNjsXNEu/LRLI98mtH/C6lnFg7kvaqPIzPn124Yd4eT/43lyqrejpc5Wb6BHq3fdv4S8Rw==
|
||||||
|
dependencies:
|
||||||
|
"@types/trusted-types" "^2.0.2"
|
||||||
|
|
||||||
|
lit@^2.0.0:
|
||||||
|
version "2.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/lit/-/lit-2.6.1.tgz#5951a2098b9bde5b328c73b55c15fdc0eefd96d7"
|
||||||
|
integrity sha512-DT87LD64f8acR7uVp7kZfhLRrHkfC/N4BVzAtnw9Yg8087mbBJ//qedwdwX0kzDbxgPccWRW6mFwGbRQIxy0pw==
|
||||||
|
dependencies:
|
||||||
|
"@lit/reactive-element" "^1.6.0"
|
||||||
|
lit-element "^3.2.0"
|
||||||
|
lit-html "^2.6.0"
|
||||||
|
|
||||||
lit@^2.1.1, lit@^2.3.1:
|
lit@^2.1.1, lit@^2.3.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/lit/-/lit-2.3.1.tgz#2cf1c2042da1e44c7a7cc72dff2d72303fd26f48"
|
resolved "https://registry.yarnpkg.com/lit/-/lit-2.3.1.tgz#2cf1c2042da1e44c7a7cc72dff2d72303fd26f48"
|
||||||
|
|||||||
Reference in New Issue
Block a user