Add support for 'scroll up refresh' in the gallery.

This commit is contained in:
Dermot Duffy
2023-03-06 21:04:05 -08:00
parent e5445284e5
commit 7f1fcf454e
11 changed files with 245 additions and 52 deletions
+6
View File
@@ -23,6 +23,7 @@ import {
CameraEndpoints,
MediaMetadataQuery,
MediaMetadataQueryResultsMap,
EngineOptions,
} from './types';
export const CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT = 10000;
@@ -58,18 +59,21 @@ export interface CameraManagerEngine {
hass: HomeAssistant,
cameras: CameraConfigs,
query: EventQuery,
engineOptions?: EngineOptions,
): Promise<EventQueryResultsMap | null>;
getRecordings(
hass: HomeAssistant,
cameras: CameraConfigs,
query: RecordingQuery,
engineOptions?: EngineOptions,
): Promise<RecordingQueryResultsMap | null>;
getRecordingSegments(
hass: HomeAssistant,
cameras: CameraConfigs,
query: RecordingSegmentsQuery,
engineOptions?: EngineOptions,
): Promise<RecordingSegmentsQueryResultsMap | null>;
generateMediaFromEvents(
@@ -102,12 +106,14 @@ export interface CameraManagerEngine {
cameras: CameraConfigs,
media: ViewMedia,
target: Date,
engineOptions?: EngineOptions,
): Promise<number | null>;
getMediaMetadata(
hass: HomeAssistant,
cameras: CameraConfigs,
query: MediaMetadataQuery,
engineOptions?: EngineOptions,
): Promise<MediaMetadataQueryResultsMap | null>;
getCameraMetadata(
+38 -13
View File
@@ -39,6 +39,7 @@ import {
MediaMetadataQuery,
MediaMetadataQueryResults,
MediaMetadataQueryResultsMap,
EngineOptions,
} from '../types';
import {
FrigateEventQueryResults,
@@ -426,6 +427,7 @@ export class FrigateCameraManagerEngine
hass: HomeAssistant,
cameras: CameraConfigs,
query: EventQuery,
engineOptions?: EngineOptions,
): Promise<EventQueryResultsMap | null> {
const output: EventQueryResultsMap = new Map();
@@ -437,7 +439,8 @@ export class FrigateCameraManagerEngine
return;
}
const instanceQuery = { ...query, cameraIDs: cameraIDs };
const cachedResult = this._requestCache.get(instanceQuery);
const cachedResult =
engineOptions?.useCache ?? true ? this._requestCache.get(instanceQuery) : null;
if (cachedResult) {
output.set(query, cachedResult as EventQueryResults);
return;
@@ -467,7 +470,9 @@ export class FrigateCameraManagerEngine
cached: false,
};
this._requestCache.set(query, { ...result, cached: true }, result.expiry);
if (engineOptions?.useCache ?? true) {
this._requestCache.set(query, { ...result, cached: true }, result.expiry);
}
output.set(instanceQuery, result);
};
@@ -491,6 +496,7 @@ export class FrigateCameraManagerEngine
hass: HomeAssistant,
cameras: CameraConfigs,
query: RecordingQuery,
engineOptions?: EngineOptions,
): Promise<RecordingQueryResultsMap | null> {
const output: RecordingQueryResultsMap = new Map();
@@ -499,7 +505,8 @@ export class FrigateCameraManagerEngine
cameraID: string,
): Promise<void> => {
const query = { ...baseQuery, cameraIDs: new Set([cameraID]) };
const cachedResult = this._requestCache.get(query);
const cachedResult =
engineOptions?.useCache ?? true ? this._requestCache.get(query) : null;
if (cachedResult) {
output.set(query, cachedResult as RecordingQueryResults);
return;
@@ -557,7 +564,9 @@ export class FrigateCameraManagerEngine
}),
cached: false,
};
this._requestCache.set(query, { ...result, cached: true }, result.expiry);
if (engineOptions?.useCache ?? true) {
this._requestCache.set(query, { ...result, cached: true }, result.expiry);
}
output.set(query, result);
};
@@ -573,6 +582,7 @@ export class FrigateCameraManagerEngine
hass: HomeAssistant,
cameras: CameraConfigs,
query: RecordingSegmentsQuery,
engineOptions?: EngineOptions,
): Promise<RecordingSegmentsQueryResultsMap | null> {
const output: RecordingSegmentsQueryResultsMap = new Map();
@@ -595,7 +605,10 @@ export class FrigateCameraManagerEngine
// query is different -- the segments won't be). This is since the
// volume of data in segment transfers can be high, and the segments can
// be used in high frequency situations (e.g. video seeking).
const cachedSegments = this._recordingSegmentsCache.get(cameraID, range);
const cachedSegments =
engineOptions?.useCache ?? true
? this._recordingSegmentsCache.get(cameraID, range)
: null;
if (cachedSegments) {
output.set(query, <FrigateRecordingSegmentsQueryResults>{
type: QueryResultsType.RecordingSegments,
@@ -615,7 +628,10 @@ export class FrigateCameraManagerEngine
};
const segments = await getRecordingSegments(hass, request);
this._recordingSegmentsCache.add(cameraID, range, segments);
if (engineOptions?.useCache ?? true) {
this._recordingSegmentsCache.add(cameraID, range, segments);
}
output.set(query, <FrigateRecordingSegmentsQueryResults>{
type: QueryResultsType.RecordingSegments,
@@ -757,6 +773,7 @@ export class FrigateCameraManagerEngine
cameras: CameraConfigs,
media: ViewMedia,
target: Date,
engineOptions?: EngineOptions,
): Promise<number | null> {
const start = media.getStartTime();
const end = media.getEndTime();
@@ -772,7 +789,7 @@ export class FrigateCameraManagerEngine
type: QueryType.RecordingSegments,
};
const results = await this.getRecordingSegments(hass, cameras, query);
const results = await this.getRecordingSegments(hass, cameras, query, engineOptions);
if (results) {
return this._getSeekTimeInSegments(
@@ -811,9 +828,10 @@ export class FrigateCameraManagerEngine
hass: HomeAssistant,
cameras: CameraConfigs,
query: MediaMetadataQuery,
engineOptions?: EngineOptions,
): Promise<MediaMetadataQueryResultsMap | null> {
const output: MediaMetadataQueryResultsMap = new Map();
if (this._requestCache.has(query)) {
if ((engineOptions?.useCache ?? true) && this._requestCache.has(query)) {
const cachedResult = <MediaMetadataQueryResults | null>(
this._requestCache.get(query)
);
@@ -860,10 +878,15 @@ export class FrigateCameraManagerEngine
};
const processRecordings = async (cameraIDs: Set<string>): Promise<void> => {
const recordings = await this.getRecordings(hass, cameras, {
type: QueryType.Recording,
cameraIDs: cameraIDs,
});
const recordings = await this.getRecordings(
hass,
cameras,
{
type: QueryType.Recording,
cameraIDs: cameraIDs,
},
engineOptions,
);
if (!recordings) {
return;
}
@@ -902,7 +925,9 @@ export class FrigateCameraManagerEngine
cached: false,
};
this._requestCache.set(query, { ...result, cached: true }, result.expiry);
if (engineOptions?.useCache ?? true) {
this._requestCache.set(query, { ...result, cached: true }, result.expiry);
}
output.set(query, result);
return output;
}
+6 -1
View File
@@ -9,7 +9,6 @@ import {
DataQuery,
EventQuery,
EventQueryResultsMap,
MediaMetadata,
PartialEventQuery,
PartialRecordingQuery,
PartialRecordingSegmentsQuery,
@@ -25,6 +24,7 @@ import {
CameraEndpoints,
MediaMetadataQuery,
MediaMetadataQueryResultsMap,
EngineOptions,
} from '../types';
import { getEntityIcon, getEntityTitle } from '../../utils/ha';
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
@@ -71,6 +71,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
_hass: HomeAssistant,
_cameras: CameraConfigs,
_query: EventQuery,
_engineOptions?: EngineOptions,
): Promise<EventQueryResultsMap | null> {
return null;
}
@@ -79,6 +80,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
_hass: HomeAssistant,
_cameras: CameraConfigs,
_query: RecordingQuery,
_engineOptions?: EngineOptions,
): Promise<RecordingQueryResultsMap | null> {
return null;
}
@@ -87,6 +89,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
_hass: HomeAssistant,
_cameras: CameraConfigs,
_query: RecordingSegmentsQuery,
_engineOptions?: EngineOptions,
): Promise<RecordingSegmentsQueryResultsMap | null> {
return null;
}
@@ -134,6 +137,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
_cameras: CameraConfigs,
_media: ViewMedia,
_target: Date,
_engineOptions?: EngineOptions,
): Promise<number | null> {
return null;
}
@@ -142,6 +146,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
_hass: HomeAssistant,
_cameras: CameraConfigs,
_query: MediaMetadataQuery,
_engineOptions?: EngineOptions,
): Promise<MediaMetadataQueryResultsMap | null> {
return null;
}
+16 -5
View File
@@ -33,6 +33,7 @@ import {
Engine,
MediaMetadataQuery,
MediaMetadataQueryResults,
EngineOptions,
} from './types.js';
import orderBy from 'lodash-es/orderBy';
import { CameraManagerEngineFactory } from './engine-factory.js';
@@ -361,31 +362,35 @@ export class CameraManager {
public async getEvents(
hass: HomeAssistant,
query: EventQuery | EventQuery[],
engineOptions?: EngineOptions,
): Promise<EventQueryResultsMap> {
return await this._handleQuery(hass, query);
return await this._handleQuery(hass, query, engineOptions);
}
public async getRecordings(
hass: HomeAssistant,
query: RecordingQuery | RecordingQuery[],
engineOptions?: EngineOptions,
): Promise<RecordingQueryResultsMap> {
return await this._handleQuery(hass, query);
return await this._handleQuery(hass, query, engineOptions);
}
public async getRecordingSegments(
hass: HomeAssistant,
query: RecordingSegmentsQuery | RecordingSegmentsQuery[],
engineOptions?: EngineOptions,
): Promise<RecordingSegmentsQueryResultsMap> {
return await this._handleQuery(hass, query);
return await this._handleQuery(hass, query, engineOptions);
}
public async executeMediaQueries<T extends MediaQuery>(
hass: HomeAssistant,
queries: T[],
engineOptions?: EngineOptions,
): Promise<ViewMedia[] | null> {
return this._convertQueryResultsToMedia(
hass,
await this._handleQuery(hass, queries),
await this._handleQuery(hass, queries, engineOptions),
);
}
@@ -394,6 +399,7 @@ export class CameraManager {
queries: T[],
results: ViewMedia[],
direction: 'earlier' | 'later',
engineOptions?: EngineOptions,
): Promise<ExtendedMediaQueryResult<T> | null> {
const getTimeFromResults = (want: 'earliest' | 'latest'): Date | null => {
let output: Date | null = null;
@@ -446,7 +452,7 @@ export class CameraManager {
const newChunkMedia = this._convertQueryResultsToMedia(
hass,
await this._handleQuery(hass, newChunkQueries),
await this._handleQuery(hass, newChunkQueries, engineOptions),
);
if (!newChunkMedia.length) {
@@ -554,6 +560,7 @@ export class CameraManager {
protected async _handleQuery<QT extends DataQuery>(
hass: HomeAssistant,
query: QT | QT[],
engineOptions?: EngineOptions,
): Promise<Map<QT, QueryReturnType<QT>>> {
const _queries = arrayify(query);
const results = new Map<QT, QueryReturnType<QT>>();
@@ -573,24 +580,28 @@ export class CameraManager {
hass,
this._store.getCameras(),
query,
engineOptions,
)) as Map<QT, QueryReturnType<QT>> | null;
} else if (QueryClassifier.isRecordingQuery(query)) {
engineResult = (await engine.getRecordings(
hass,
this._store.getCameras(),
query,
engineOptions,
)) as Map<QT, QueryReturnType<QT>> | null;
} else if (QueryClassifier.isRecordingSegmentsQuery(query)) {
engineResult = (await engine.getRecordingSegments(
hass,
this._store.getCameras(),
query,
engineOptions,
)) as Map<QT, QueryReturnType<QT>> | null;
} else if (QueryClassifier.isMediaMetadataQuery(query)) {
engineResult = (await engine.getMediaMetadata(
hass,
this._store.getCameras(),
query,
engineOptions,
)) as Map<QT, QueryReturnType<QT>> | null;
}
+4
View File
@@ -130,6 +130,10 @@ export interface CameraEndpoints {
export type CameraConfigs = Map<string, CameraConfig>;
export interface EngineOptions {
useCache?: boolean;
}
// ===========
// Event Query
// ===========
+135 -14
View File
@@ -31,17 +31,21 @@ import { MediaQueriesClassifier } from '../view/media-queries-classifier';
import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries';
import { EventQuery, MediaQuery, RecordingQuery } from '../camera-manager/types';
import { MediaQueriesResults } from '../view/media-queries-results';
import { errorToConsole } from '../utils/basic';
import { errorToConsole, sleep } from '../utils/basic';
import './media-filter';
import './surround-basic';
import { ViewMedia } from '../view/media';
import { localize } from '../localize/localize';
import throttle from 'lodash-es/throttle';
import { classMap } from 'lit/directives/class-map.js';
const GALLERY_MEDIA_FILTER_MENU_ICONS = {
closed: 'mdi:filter-cog-outline',
open: 'mdi:filter-cog',
};
const MIN_GALLERY_EXTENSION_SECONDS = 0.5;
@customElement('frigate-card-gallery')
export class FrigateCardGallery extends LitElement {
@property({ attribute: false })
@@ -167,14 +171,42 @@ export class FrigateCardGalleryCore extends LitElement {
protected _intersectionObserver: IntersectionObserver;
protected _resizeObserver: ResizeObserver;
protected _refLoader: Ref<HTMLElement> = createRef();
protected _refLoaderBottom: Ref<HTMLElement> = createRef();
protected _refSelected: Ref<HTMLElement> = createRef();
// Bottom loader: A progress indicator shown in a "cell" (not across) at the
// bottom of the gallery. Once visible this attempts to fetch new content from
// "earlier" (less recently) than the current query. This is rendered by
// default (and once visible, the fetch is triggered after which it is
// re-hidden).
@state()
protected _showExtensionLoader = true;
protected _showLoaderBottom = true;
// Top loader: A progress indicator is shown across the top of the gallery if
// the user is _already_ at the top of the gallery and scrolls upwards. This
// attempts to fetch new content from "later" (more recently) than the current
// query. This is hidden by default.
@state()
protected _showLoaderTop = false;
protected _media?: ViewMedia[];
protected _boundWheelHandler = this._wheelHandler.bind(this);
protected _boundTouchStartHandler = this._touchStartHandler.bind(this);
protected _boundTouchEndHandler = this._touchEndHandler.bind(this);
// Wheel / touch events may be voluminous, throttle extension calls.
protected _throttleExtendGalleryLater = throttle(
this._extendGallery.bind(this),
MIN_GALLERY_EXTENSION_SECONDS * 1000,
{
leading: true,
trailing: false,
},
);
protected _touchScrollYPosition: number | null = null;
constructor() {
super();
this._resizeObserver = new ResizeObserver(this._resizeHandler.bind(this));
@@ -183,12 +215,72 @@ export class FrigateCardGalleryCore extends LitElement {
);
}
// Since the scroll event does not fire if the user is already at the top of
// the container, instead we manually use the wheel and touchstart/end events
// to detect "top upwards scrolling" (to trigger an extension of the gallery).
protected _touchStartHandler(ev: TouchEvent): void {
// Remember the Y touch position on touch start, so that we can calculate if
// the user gestured upwards or downards on touchend.
if (ev.touches.length === 1) {
this._touchScrollYPosition = ev.touches[0].screenY;
} else {
this._touchScrollYPosition = null;
}
}
protected async _touchEndHandler(ev: TouchEvent): Promise<void> {
if (
!this.scrollTop &&
ev.changedTouches.length === 1 &&
this._touchScrollYPosition
) {
if (ev.changedTouches[0].screenY > this._touchScrollYPosition) {
await this._extendLater();
}
}
this._touchScrollYPosition = null;
}
protected async _wheelHandler(ev: WheelEvent): Promise<void> {
if (!this.scrollTop && ev.deltaY < 0) {
await this._extendLater();
}
}
protected async _extendLater(): Promise<void> {
const start = new Date();
this._showLoaderTop = true;
await this._throttleExtendGalleryLater(
'later',
// Ask the engine to avoid use of cache since the user is explicitly
// looking for the freshest possible data.
false,
);
const delta = new Date().getTime() - start.getTime();
if (delta < MIN_GALLERY_EXTENSION_SECONDS * 1000) {
// Hidden gem: "legitimate" (?!) use of sleep() :-)
// These calls can return very quickly even with caching disabled since
// the time window constraints on the query will usually be very narrow
// and the backend can thus very quickly reply. It's often so fast it
// actually looks like a rendering issue where the progress indictor
// barely registers before it's gone again. This optional pause ensures
// there is at least some visual feedback to the user that last long
// enough they can 'feel' the fetch has happened.
await sleep(MIN_GALLERY_EXTENSION_SECONDS - delta / 1000);
}
this._showLoaderTop = false;
}
/**
* Component connected callback.
*/
connectedCallback(): void {
super.connectedCallback();
this._resizeObserver.observe(this);
this.addEventListener('wheel', this._boundWheelHandler, { passive: true });
this.addEventListener('touchstart', this._boundTouchStartHandler, { passive: true });
this.addEventListener('touchend', this._boundTouchEndHandler);
// Request update in order to ensure the intersection observer reconnects
// with the loader sentinel.
@@ -199,6 +291,9 @@ export class FrigateCardGalleryCore extends LitElement {
* Component disconnected callback.
*/
disconnectedCallback(): void {
this.removeEventListener('wheel', this._boundWheelHandler);
this.removeEventListener('touchstart', this._boundTouchStartHandler);
this.removeEventListener('touchend', this._boundTouchEndHandler);
this._resizeObserver.disconnect();
this._intersectionObserver.disconnect();
super.disconnectedCallback();
@@ -232,14 +327,21 @@ export class FrigateCardGalleryCore extends LitElement {
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;
this._showLoaderBottom = false;
await this._extendGallery('earlier');
}
protected async _extendGallery(
direction: 'earlier' | 'later',
useCache = true,
): Promise<void> {
if (!this.cameraManager || !this.hass || !this.view) {
return;
}
const query = this.view?.query;
const rawQueries = query?.getQueries() ?? null;
@@ -254,7 +356,10 @@ export class FrigateCardGalleryCore extends LitElement {
this.hass,
rawQueries,
existingMedia,
'earlier',
direction,
{
useCache: useCache,
},
);
} catch (e) {
errorToConsole(e as Error);
@@ -272,7 +377,9 @@ export class FrigateCardGalleryCore extends LitElement {
this.view
?.evolve({
query: newMediaQueries,
queryResults: new MediaQueriesResults(extension.results),
queryResults: new MediaQueriesResults(extension.results).selectResultIfFound(
(media) => media === this.view?.queryResults?.getSelectedResult(),
),
})
.dispatchChangeEvent(this);
}
@@ -299,7 +406,9 @@ export class FrigateCardGalleryCore extends LitElement {
}
}
if (changedProps.has('view')) {
this._showExtensionLoader = true;
// If the view changes, always render the bottom loader to allow for the
// view to be extended once the bottom loader becomes visible.
this._showLoaderBottom = true;
const oldView: View | undefined = changedProps.get('view');
if (
@@ -330,10 +439,22 @@ export class FrigateCardGalleryCore extends LitElement {
const selected = this.view?.queryResults?.getSelectedResult();
return html`
${this._showLoaderTop
? html`${renderProgressIndicator({
cardWideConfig: this.cardWideConfig,
classes: {
top: true,
},
size: 'small',
})}`
: ''}
${this._media.map(
(media, index) =>
html`<frigate-card-thumbnail
${media === selected ? ref(this._refSelected) : ''}
class=${classMap({
selected: media === selected,
})}
.hass=${this.hass}
.cameraManager=${this.cameraManager}
.media=${media}
@@ -361,19 +482,19 @@ export class FrigateCardGalleryCore extends LitElement {
>
</frigate-card-thumbnail>`,
)}
${this._showExtensionLoader
${this._showLoaderBottom
? html`${renderProgressIndicator({
cardWideConfig: this.cardWideConfig,
componentRef: this._refLoader,
componentRef: this._refLoaderBottom,
})}`
: ''}
`;
}
public updated(changedProps: PropertyValues): void {
if (this._refLoader.value) {
if (this._refLoaderBottom.value) {
this._intersectionObserver.disconnect();
this._intersectionObserver.observe(this._refLoader.value);
this._intersectionObserver.observe(this._refLoaderBottom.value);
}
// This wait for updateComplete is necessary for the scrolling to work
+14 -4
View File
@@ -1,6 +1,6 @@
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ClassInfo, classMap } from 'lit/directives/class-map.js';
import { ref, Ref } from 'lit/directives/ref.js';
import { TROUBLESHOOTING_URL } from '../const.js';
import { localize } from '../localize/localize.js';
@@ -29,7 +29,7 @@ export class FrigateCardMessage extends LitElement {
dotdotdot: !!this.dotdotdot,
};
return html` <div class="wrapper">
<div class="message">
<div class="message padded">
<div class="icon">
<ha-icon icon="${icon}"> </ha-icon>
</div>
@@ -78,6 +78,8 @@ export class FrigateCardErrorMessage extends LitElement {
}
}
type FrigateCardProgressIndicatorSize = 'tiny' | 'small' | 'medium' | 'large';
@customElement('frigate-card-progress-indicator')
export class FrigateCardProgressIndicator extends LitElement {
@property({ attribute: false })
@@ -86,10 +88,14 @@ export class FrigateCardProgressIndicator extends LitElement {
@property({ attribute: false })
public animated = false;
@property({ attribute: false })
public size: FrigateCardProgressIndicatorSize = 'large';
protected render(): TemplateResult {
return html` <div class="message vertical">
${this.animated
? html`<ha-circular-progress active="true" size="large"> </ha-circular-progress>`
? html`<ha-circular-progress active="true" size="${this.size}">
</ha-circular-progress>`
: html`<ha-icon icon="mdi:timer-sand"></ha-icon>`}
${this.message ? html`<span>${this.message}</span>` : html``}
</div>`;
@@ -119,10 +125,14 @@ export function renderMessage(message: Message): TemplateResult {
export function renderProgressIndicator(options?: {
message?: string;
cardWideConfig?: CardWideConfig;
componentRef?: Ref<HTMLElement>,
componentRef?: Ref<HTMLElement>;
classes?: ClassInfo;
size?: FrigateCardProgressIndicatorSize;
}): TemplateResult {
return html`
<frigate-card-progress-indicator
class="${classMap(options?.classes ?? {})}"
.size=${options?.size}
${options?.componentRef ? ref(options.componentRef) : ''}
.message=${options?.message || ''}
.animated=${options?.cardWideConfig?.performance?.features
+16
View File
@@ -30,3 +30,19 @@ frigate-card-thumbnail {
frigate-card-thumbnail:not([details]) {
width: 100%;
}
frigate-card-thumbnail.selected {
border: 4px solid var(--accent-color);
// Because this is box-sizing: border-box, the border is effectively
// 'padding'. To get the curved borders to line up between the thumbnail and
// this outer border, we need to add the size of the border to the thumbnail
// image border radius.
// Related: https://www.30secondsofcode.org/articles/s/css-nested-border-radius
border-radius: calc(
var(--frigate-card-css-border-radius, var(--ha-card-border-radius, 4px)) + 4px
);
}
frigate-card-progress-indicator.top {
// The top loading progress indicator should span the whole width.
grid-column: 1/-1;
}
+4 -5
View File
@@ -24,10 +24,13 @@ div.message {
justify-content: center;
align-items: center;
box-sizing: border-box;
padding: 20px;
height: 100%;
}
div.message.padded {
padding: 20px;
}
div.message div.contents {
display: flex;
flex-direction: column;
@@ -55,7 +58,3 @@ div.message div.icon {
white-space: pre-wrap;
word-break: break-all;
}
.message ha-icon, ha-circular-progress {
padding: 10px;
}
+5
View File
@@ -191,3 +191,8 @@ export const isSuperset = (superset: Set<unknown>, subset: Set<unknown>) => {
}
return true;
};
// Usage of this function needs to be justified with a comment.
export const sleep = async (seconds: number) => {
await new Promise((r) => setTimeout(r, seconds * 1000));
};
-9
View File
@@ -5,12 +5,3 @@ export const log = (cardWideConfig?: CardWideConfig, ...args: unknown[]) => {
console.debug(...args);
}
};
/**
* For debug purposes only.
* @param seconds
*/
// ts-prune-ignore-next
export const sleep = async (seconds: number) => {
await new Promise((r) => setTimeout(r, seconds * 1000));
};