Add 'infinite' gallery support.
This commit is contained in:
+117
-38
@@ -5,6 +5,7 @@ import {
|
|||||||
DataQuery,
|
DataQuery,
|
||||||
EventQuery,
|
EventQuery,
|
||||||
EventQueryResults,
|
EventQueryResults,
|
||||||
|
MediaQuery,
|
||||||
PartialDataQuery,
|
PartialDataQuery,
|
||||||
PartialEventQuery,
|
PartialEventQuery,
|
||||||
PartialQueryConcreteType,
|
PartialQueryConcreteType,
|
||||||
@@ -61,6 +62,11 @@ export class QueryResultClassifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExtendedMediaQueryResult<T extends MediaQuery> {
|
||||||
|
queries: T[];
|
||||||
|
results: ViewMedia[];
|
||||||
|
}
|
||||||
|
|
||||||
export type RequestCache = MemoryRequestCache<DataQuery, QueryResults>;
|
export type RequestCache = MemoryRequestCache<DataQuery, QueryResults>;
|
||||||
|
|
||||||
export class CameraManager {
|
export class CameraManager {
|
||||||
@@ -171,57 +177,89 @@ export class CameraManager {
|
|||||||
return await this._handleQuery(hass, query);
|
return await this._handleQuery(hass, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async executeMediaQuery(
|
public async executeMediaQueries(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mediaQuerys: MediaQueries,
|
mediaQueries: MediaQueries,
|
||||||
): Promise<MediaQueriesResults | null> {
|
): Promise<MediaQueriesResults | null> {
|
||||||
const queries: (RecordingQuery | EventQuery)[] | null = mediaQuerys.getQueries();
|
const queries: (RecordingQuery | EventQuery)[] | null = mediaQueries.getQueries();
|
||||||
if (!queries) {
|
if (!queries) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const mediaArray = this._convertQueryResultsToMedia(
|
||||||
const results = await this._handleQuery(hass, queries);
|
await this._handleQuery(hass, queries),
|
||||||
|
);
|
||||||
const mediaArray: ViewMedia[] = [];
|
|
||||||
for (const [query, result] of results.entries()) {
|
|
||||||
const cameraConfig = this._cameras.get(query.cameraID);
|
|
||||||
const engine = this._engineFactory.getEngineForCamera(cameraConfig);
|
|
||||||
|
|
||||||
if (engine && cameraConfig) {
|
|
||||||
let media: ViewMedia[] | null = null;
|
|
||||||
if (
|
|
||||||
QueryClassifier.isEventQuery(query) &&
|
|
||||||
QueryResultClassifier.isEventQueryResult(result)
|
|
||||||
) {
|
|
||||||
media = engine.generateMediaFromEvents(cameraConfig, query, result);
|
|
||||||
} else if (
|
|
||||||
QueryClassifier.isRecordingQuery(query) &&
|
|
||||||
QueryResultClassifier.isRecordingQuery(result)
|
|
||||||
) {
|
|
||||||
media = engine.generateMediaFromRecordings(cameraConfig, query, result);
|
|
||||||
}
|
|
||||||
if (media) {
|
|
||||||
mediaArray.push(...media);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new MediaQueriesResults(
|
return new MediaQueriesResults(
|
||||||
orderBy(
|
mediaArray,
|
||||||
// Ensure uniqueness by the ID (if specified), otherwise all elements
|
|
||||||
// are assumed to be unique.
|
|
||||||
uniqBy(mediaArray, (media) => media.getID() ?? media),
|
|
||||||
|
|
||||||
// Sort all items leading with the most recent.
|
|
||||||
(media) => media.getStartTime(),
|
|
||||||
'desc',
|
|
||||||
),
|
|
||||||
|
|
||||||
// Select the first (most-recent) item.
|
// Select the first (most-recent) item.
|
||||||
mediaArray.length ? 0 : null,
|
mediaArray.length ? 0 : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async extendMediaQueries<T extends MediaQuery>(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
queries: T[],
|
||||||
|
results: ViewMedia[],
|
||||||
|
direction: 'earlier' | 'later',
|
||||||
|
chunkSize: number,
|
||||||
|
): Promise<ExtendedMediaQueryResult<T> | null> {
|
||||||
|
const getTimeFromResults = (want: 'earliest' | 'latest'): Date | null => {
|
||||||
|
let output: Date | null = null;
|
||||||
|
for (const result of results) {
|
||||||
|
const startTime = result.getStartTime();
|
||||||
|
if (
|
||||||
|
startTime &&
|
||||||
|
(!output ||
|
||||||
|
(want === 'earliest' && startTime < output) ||
|
||||||
|
(want === 'latest' && startTime > output))
|
||||||
|
) {
|
||||||
|
output = startTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The queries associated with the chunk to fetch.
|
||||||
|
const newChunkQueries: T[] = [];
|
||||||
|
|
||||||
|
// The re-constituted combined query.
|
||||||
|
const newCombinedQueries: T[] = [];
|
||||||
|
|
||||||
|
for (const query of queries) {
|
||||||
|
const newChunkQuery = { ...query };
|
||||||
|
const newCombinedQuery = { ...query };
|
||||||
|
|
||||||
|
if (direction === 'later') {
|
||||||
|
newChunkQuery.start = getTimeFromResults('latest') ?? undefined;
|
||||||
|
newChunkQuery.end = undefined;
|
||||||
|
newCombinedQuery.end = undefined;
|
||||||
|
} else if (direction === 'earlier') {
|
||||||
|
newChunkQuery.end = getTimeFromResults('earliest') ?? undefined;
|
||||||
|
newChunkQuery.start = undefined;
|
||||||
|
newCombinedQuery.start = undefined;
|
||||||
|
}
|
||||||
|
newChunkQuery.limit = chunkSize;
|
||||||
|
newCombinedQuery.limit = (query.limit ?? 0) + chunkSize;
|
||||||
|
|
||||||
|
newCombinedQueries.push(newCombinedQuery);
|
||||||
|
newChunkQueries.push(newChunkQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newChunkMedia = this._convertQueryResultsToMedia(
|
||||||
|
await this._handleQuery(hass, newChunkQueries),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!newChunkMedia.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
queries: newCombinedQueries,
|
||||||
|
results: this._sortMedia(results.concat(newChunkMedia)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public getMediaDownloadPath(media: ViewMedia): string | null {
|
public getMediaDownloadPath(media: ViewMedia): string | null {
|
||||||
const cameraConfig = this._cameras.get(media.getCameraID());
|
const cameraConfig = this._cameras.get(media.getCameraID());
|
||||||
const engine = cameraConfig
|
const engine = cameraConfig
|
||||||
@@ -362,4 +400,45 @@ export class CameraManager {
|
|||||||
);
|
);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _convertQueryResultsToMedia<QT extends DataQuery>(
|
||||||
|
results: Map<QT, QueryReturnType<QT>>,
|
||||||
|
): ViewMedia[] {
|
||||||
|
const mediaArray: ViewMedia[] = [];
|
||||||
|
for (const [query, result] of results.entries()) {
|
||||||
|
const cameraConfig = this._cameras.get(query.cameraID);
|
||||||
|
const engine = this._engineFactory.getEngineForCamera(cameraConfig);
|
||||||
|
|
||||||
|
if (engine && cameraConfig) {
|
||||||
|
let media: ViewMedia[] | null = null;
|
||||||
|
if (
|
||||||
|
QueryClassifier.isEventQuery(query) &&
|
||||||
|
QueryResultClassifier.isEventQueryResult(result)
|
||||||
|
) {
|
||||||
|
media = engine.generateMediaFromEvents(cameraConfig, query, result);
|
||||||
|
} else if (
|
||||||
|
QueryClassifier.isRecordingQuery(query) &&
|
||||||
|
QueryResultClassifier.isRecordingQuery(result)
|
||||||
|
) {
|
||||||
|
media = engine.generateMediaFromRecordings(cameraConfig, query, result);
|
||||||
|
}
|
||||||
|
if (media) {
|
||||||
|
mediaArray.push(...media);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this._sortMedia(mediaArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _sortMedia(mediaArray: ViewMedia[]): ViewMedia[] {
|
||||||
|
return orderBy(
|
||||||
|
// Ensure uniqueness by the ID (if specified), otherwise all elements
|
||||||
|
// are assumed to be unique.
|
||||||
|
uniqBy(mediaArray, (media) => media.getID() ?? media),
|
||||||
|
|
||||||
|
// Sort all items leading with the most recent.
|
||||||
|
(media) => media.getStartTime(),
|
||||||
|
'desc',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -1021,9 +1021,11 @@ export class FrigateCard extends LitElement {
|
|||||||
if (View.isMediaChange(this._view, view)) {
|
if (View.isMediaChange(this._view, view)) {
|
||||||
this._currentMediaLoadedInfo = null;
|
this._currentMediaLoadedInfo = null;
|
||||||
}
|
}
|
||||||
|
if (this._view?.view !== view.view) {
|
||||||
|
this._resetMainScroll();
|
||||||
|
}
|
||||||
this._view = view;
|
this._view = view;
|
||||||
this._generateConditionState();
|
this._generateConditionState();
|
||||||
this._resetMainScroll();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (args?.resetMessage ?? true) {
|
if (args?.resetMessage ?? true) {
|
||||||
|
|||||||
+118
-29
@@ -7,7 +7,7 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
unsafeCSS,
|
unsafeCSS,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import galleryStyle from '../scss/gallery.scss';
|
import galleryStyle from '../scss/gallery.scss';
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
@@ -22,12 +22,20 @@ import {
|
|||||||
changeViewToRecentEventsForCameraAndDependents,
|
changeViewToRecentEventsForCameraAndDependents,
|
||||||
changeViewToRecentRecordingForCameraAndDependents,
|
changeViewToRecentRecordingForCameraAndDependents,
|
||||||
} from '../utils/media-to-view.js';
|
} from '../utils/media-to-view.js';
|
||||||
import { CameraManager } from '../camera/manager.js';
|
import { CameraManager, ExtendedMediaQueryResult } from '../camera/manager.js';
|
||||||
import { View } from '../view/view.js';
|
import { View } from '../view/view.js';
|
||||||
import { renderProgressIndicator } from './message.js';
|
import { renderProgressIndicator } from './message.js';
|
||||||
import './thumbnail.js';
|
import './thumbnail.js';
|
||||||
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
||||||
import './media-filter';
|
import './media-filter';
|
||||||
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
|
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
|
||||||
|
import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries';
|
||||||
|
import { EventQuery, MediaQuery, RecordingQuery } from '../camera/types';
|
||||||
|
import { MediaQueriesResults } from '../view/media-queries-results';
|
||||||
|
import { errorToConsole } from '../utils/basic';
|
||||||
|
|
||||||
|
const GALLERY_MEDIA_CHUNK_SIZE = 100;
|
||||||
|
|
||||||
@customElement('frigate-card-gallery')
|
@customElement('frigate-card-gallery')
|
||||||
export class FrigateCardGallery extends LitElement {
|
export class FrigateCardGallery extends LitElement {
|
||||||
@@ -136,11 +144,19 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraManager?: CameraManager;
|
public cameraManager?: CameraManager;
|
||||||
|
|
||||||
|
protected _intersectionObserver: IntersectionObserver;
|
||||||
protected _resizeObserver: ResizeObserver;
|
protected _resizeObserver: ResizeObserver;
|
||||||
|
protected _refSentinel: Ref<HTMLElement> = createRef();
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _showExtensionLoader = true;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this._resizeObserver = new ResizeObserver(this._resizeHandler.bind(this));
|
this._resizeObserver = new ResizeObserver(this._resizeHandler.bind(this));
|
||||||
|
this._intersectionObserver = new IntersectionObserver(
|
||||||
|
this._intersectionHandler.bind(this),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -149,6 +165,10 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._resizeObserver.observe(this);
|
this._resizeObserver.observe(this);
|
||||||
|
|
||||||
|
// Request update in order to ensure the intersection observer reconnects
|
||||||
|
// with the sentinel.
|
||||||
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -156,6 +176,7 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
*/
|
*/
|
||||||
disconnectedCallback(): void {
|
disconnectedCallback(): void {
|
||||||
this._resizeObserver.disconnect();
|
this._resizeObserver.disconnect();
|
||||||
|
this._intersectionObserver.disconnect();
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +205,57 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
this._setColumnCount();
|
this._setColumnCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async _intersectionHandler(
|
||||||
|
entries: IntersectionObserverEntry[],
|
||||||
|
): Promise<void> {
|
||||||
|
if (!this.cameraManager || !this.hass || !this.view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entries.every((entry) => !entry.isIntersecting)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._showExtensionLoader = false;
|
||||||
|
|
||||||
|
const query = this.view?.query;
|
||||||
|
const rawQueries: MediaQuery[] | null = query?.getQueries() ?? null;
|
||||||
|
const existingMedia = this.view.queryResults?.getResults();
|
||||||
|
if (!query || !rawQueries || !existingMedia) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let extension: ExtendedMediaQueryResult<MediaQuery> | null;
|
||||||
|
try {
|
||||||
|
extension = await this.cameraManager.extendMediaQueries(
|
||||||
|
this.hass,
|
||||||
|
rawQueries,
|
||||||
|
existingMedia,
|
||||||
|
'earlier',
|
||||||
|
GALLERY_MEDIA_CHUNK_SIZE,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
errorToConsole(e as Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension) {
|
||||||
|
const newMediaQueries = MediaQueriesClassifier.areEventQueries(query)
|
||||||
|
? new EventMediaQueries(extension.queries as EventQuery[])
|
||||||
|
: MediaQueriesClassifier.areRecordingQueries(query)
|
||||||
|
? new RecordingMediaQueries(extension.queries as RecordingQuery[])
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (newMediaQueries) {
|
||||||
|
this.view
|
||||||
|
?.evolve({
|
||||||
|
query: newMediaQueries,
|
||||||
|
queryResults: new MediaQueriesResults(extension.results),
|
||||||
|
})
|
||||||
|
.dispatchChangeEvent(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an update will occur.
|
* Called when an update will occur.
|
||||||
* @param changedProps The changed properties
|
* @param changedProps The changed properties
|
||||||
@@ -203,6 +275,9 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (changedProps.has('view')) {
|
||||||
|
this._showExtensionLoader = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -222,33 +297,47 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html` ${results.map(
|
return html`
|
||||||
(media, index) =>
|
${results.map(
|
||||||
html`<frigate-card-thumbnail
|
(media, index) =>
|
||||||
.hass=${this.hass}
|
html`<frigate-card-thumbnail
|
||||||
.cameraManager=${this.cameraManager}
|
.hass=${this.hass}
|
||||||
.media=${media}
|
.cameraManager=${this.cameraManager}
|
||||||
.cameraConfig=${this.cameras?.get(media.getCameraID())}
|
.media=${media}
|
||||||
.view=${this.view}
|
.cameraConfig=${this.cameras?.get(media.getCameraID())}
|
||||||
?details=${!!this.galleryConfig?.controls.thumbnails.show_details}
|
.view=${this.view}
|
||||||
?show_favorite_control=${!!this.galleryConfig?.controls.thumbnails
|
?details=${!!this.galleryConfig?.controls.thumbnails.show_details}
|
||||||
.show_favorite_control}
|
?show_favorite_control=${!!this.galleryConfig?.controls.thumbnails
|
||||||
?show_timeline_control=${!!this.galleryConfig?.controls.thumbnails
|
.show_favorite_control}
|
||||||
.show_timeline_control}
|
?show_timeline_control=${!!this.galleryConfig?.controls.thumbnails
|
||||||
@click=${(ev: Event) => {
|
.show_timeline_control}
|
||||||
if (this.view) {
|
@click=${(ev: Event) => {
|
||||||
this.view
|
if (this.view) {
|
||||||
.evolve({
|
this.view
|
||||||
view: 'media',
|
.evolve({
|
||||||
queryResults: this.view.queryResults?.clone().selectResult(index),
|
view: 'media',
|
||||||
})
|
queryResults: this.view.queryResults?.clone().selectResult(index),
|
||||||
.dispatchChangeEvent(this);
|
})
|
||||||
}
|
.dispatchChangeEvent(this);
|
||||||
stopEventFromActivatingCardWideActions(ev);
|
}
|
||||||
}}
|
stopEventFromActivatingCardWideActions(ev);
|
||||||
>
|
}}
|
||||||
</frigate-card-thumbnail>`,
|
>
|
||||||
)}`;
|
</frigate-card-thumbnail>`,
|
||||||
|
)}
|
||||||
|
${this._showExtensionLoader
|
||||||
|
? html` <ha-card class="sentinel" ${ref(this._refSentinel)}>
|
||||||
|
<span class="dotdotdot"></span>
|
||||||
|
</ha-card>`
|
||||||
|
: ''}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updated(): void {
|
||||||
|
if (this._refSentinel.value) {
|
||||||
|
this._intersectionObserver.disconnect();
|
||||||
|
this._intersectionObserver.observe(this._refSentinel.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
|
|
||||||
let results: MediaQueriesResults | null;
|
let results: MediaQueriesResults | null;
|
||||||
try {
|
try {
|
||||||
results = await this.cameraManager.executeMediaQuery(this.hass, clipQuery);
|
results = await this.cameraManager.executeMediaQueries(this.hass, clipQuery);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorToConsole(e as Error);
|
errorToConsole(e as Error);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
.dotdotdot:after {
|
||||||
|
@keyframes dots {
|
||||||
|
0%,
|
||||||
|
20% {
|
||||||
|
content: '.';
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
content: '..';
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
content: '...';
|
||||||
|
}
|
||||||
|
90%,
|
||||||
|
100% {
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animation: dots 2s linear infinite;
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 3em;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@use 'dotdotdot.scss';
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -31,7 +33,10 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--primary-text-color, white);
|
color: var(--primary-text-color, white);
|
||||||
border: 1px solid var(--primary-color);
|
border: 1px solid var(--primary-color);
|
||||||
border-radius: var(--frigate-card-css-border-radius, var(--ha-card-border-radius, 4px));
|
border-radius: var(
|
||||||
|
--frigate-card-css-border-radius,
|
||||||
|
var(--ha-card-border-radius, 4px)
|
||||||
|
);
|
||||||
|
|
||||||
// Folder background color should match the thumbnail element background
|
// Folder background color should match the thumbnail element background
|
||||||
// color.
|
// color.
|
||||||
|
|||||||
+3
-25
@@ -1,3 +1,5 @@
|
|||||||
|
@use 'dotdotdot.scss';
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -56,28 +58,4 @@ div.message div.icon {
|
|||||||
|
|
||||||
.message ha-icon, ha-circular-progress {
|
.message ha-icon, ha-circular-progress {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dotdotdot:after {
|
|
||||||
@keyframes dots {
|
|
||||||
0%,
|
|
||||||
20% {
|
|
||||||
content: '.';
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
content: '..';
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
content: '...';
|
|
||||||
}
|
|
||||||
90%,
|
|
||||||
100% {
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
animation: dots 2s linear infinite;
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
width: 3em;
|
|
||||||
}
|
|
||||||
@@ -155,3 +155,11 @@ export function getDurationString(start: Date, end: Date): string {
|
|||||||
duration += `${seconds}s`;
|
duration += `${seconds}s`;
|
||||||
return duration;
|
return duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For debug purposes only.
|
||||||
|
* @param seconds
|
||||||
|
*/
|
||||||
|
export const sleep = async (seconds: number) => {
|
||||||
|
await new Promise(r => setTimeout(r, seconds * 1000));
|
||||||
|
}
|
||||||
@@ -63,7 +63,7 @@ export const createViewForEvents = async (
|
|||||||
|
|
||||||
let queryResults: MediaQueriesResults | null;
|
let queryResults: MediaQueriesResults | null;
|
||||||
try {
|
try {
|
||||||
queryResults = await cameraManager.executeMediaQuery(hass, query);
|
queryResults = await cameraManager.executeMediaQueries(hass, query);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorToConsole(e as Error);
|
errorToConsole(e as Error);
|
||||||
dispatchFrigateCardErrorEvent(element, e as Error);
|
dispatchFrigateCardErrorEvent(element, e as Error);
|
||||||
@@ -146,7 +146,7 @@ export const createViewForRecordings = async (
|
|||||||
let queryResults: MediaQueriesResults | null;
|
let queryResults: MediaQueriesResults | null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
queryResults = await cameraManager.executeMediaQuery(hass, query);
|
queryResults = await cameraManager.executeMediaQueries(hass, query);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorToConsole(e as Error);
|
errorToConsole(e as Error);
|
||||||
dispatchFrigateCardErrorEvent(element, e as Error);
|
dispatchFrigateCardErrorEvent(element, e as Error);
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export class TimelineDataSource {
|
|||||||
this.getTimelineEventQueries(cacheFriendlyWindow),
|
this.getTimelineEventQueries(cacheFriendlyWindow),
|
||||||
);
|
);
|
||||||
|
|
||||||
const results = await this._cameraManager.executeMediaQuery(hass, query);
|
const results = await this._cameraManager.executeMediaQueries(hass, query);
|
||||||
for (const media of results?.getResults() ?? []) {
|
for (const media of results?.getResults() ?? []) {
|
||||||
const endTime = media.getEndTime();
|
const endTime = media.getEndTime();
|
||||||
const startTime = media.getStartTime();
|
const startTime = media.getStartTime();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export type MediaQueries = EventMediaQueries | RecordingMediaQueries;
|
|||||||
export class MediaQueriesBase<T extends MediaQuery> {
|
export class MediaQueriesBase<T extends MediaQuery> {
|
||||||
protected _queries: T[] | null = null;
|
protected _queries: T[] | null = null;
|
||||||
|
|
||||||
protected constructor(queries?: T[]) {
|
public constructor(queries?: T[]) {
|
||||||
if (queries) {
|
if (queries) {
|
||||||
this._queries = queries;
|
this._queries = queries;
|
||||||
}
|
}
|
||||||
@@ -28,20 +28,9 @@ export class MediaQueriesBase<T extends MediaQuery> {
|
|||||||
public setQueries(queries: T[]): void {
|
public setQueries(queries: T[]): void {
|
||||||
this._queries = queries;
|
this._queries = queries;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setQueriesTime(start: Date, end: Date) {
|
|
||||||
for (const query of this._queries ?? []) {
|
|
||||||
query.start = start;
|
|
||||||
query.end = end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EventMediaQueries extends MediaQueriesBase<EventQuery> {
|
export class EventMediaQueries extends MediaQueriesBase<EventQuery> {
|
||||||
constructor(queries?: EventQuery[]) {
|
|
||||||
super(queries);
|
|
||||||
}
|
|
||||||
|
|
||||||
public convertToClipsQueries(): void {
|
public convertToClipsQueries(): void {
|
||||||
for (const query of this._queries ?? []) {
|
for (const query of this._queries ?? []) {
|
||||||
delete query.hasSnapshot;
|
delete query.hasSnapshot;
|
||||||
@@ -54,8 +43,4 @@ export class EventMediaQueries extends MediaQueriesBase<EventQuery> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RecordingMediaQueries extends MediaQueriesBase<RecordingQuery> {
|
export class RecordingMediaQueries extends MediaQueriesBase<RecordingQuery> {}
|
||||||
constructor(queries?: RecordingQuery[]) {
|
|
||||||
super(queries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// Minor / later:
|
// Minor / later:
|
||||||
// - TODO: ts-prune https://camchenry.com/blog/deleting-dead-code-in-typescript
|
// - TODO: ts-prune https://camchenry.com/blog/deleting-dead-code-in-typescript
|
||||||
// - TODO: getRecordingTitle should use getCameraTitle but need hass.
|
// - TODO: getRecordingTitle should use getCameraTitle but need hass.
|
||||||
|
// - TODO: Take MediaQueries wrappers out of the camera manager.
|
||||||
|
// - TODO: Event gallery show_details default does not work.
|
||||||
|
|
||||||
// Hard:
|
// Hard:
|
||||||
// - TODO: Implement gallery.
|
// - TODO: Implement gallery.
|
||||||
|
|||||||
Reference in New Issue
Block a user