feat: Add loading spinner on card initialization (#1549)

* feat: Add loading spinner on card initialization

* Minor doc fix.
This commit is contained in:
Dermot Duffy
2024-09-20 19:39:17 -07:00
committed by GitHub
parent 650d99ec00
commit b46ea36265
7 changed files with 139 additions and 28 deletions
@@ -31,12 +31,17 @@ export class InitializationManager {
// initialization" (above) are followed.
protected _initializationQueue = new PQueue({ concurrency: 1 });
protected _initializer: Initializer;
protected _everInitialized = false;
constructor(api: CardInitializerAPI, initializer?: Initializer) {
this._api = api;
this._initializer = initializer ?? new Initializer();
}
public wasEverInitialized(): boolean {
return this._everInitialized;
}
public isInitializedMandatory(): boolean {
const config = this._api.getConfigManager().getConfig();
if (!config) {
@@ -113,6 +118,7 @@ export class InitializationManager {
return;
}
this._everInitialized = true;
this._api.getCardElementManager().update();
}
+32 -27
View File
@@ -12,6 +12,7 @@ import { CardController } from './card-controller/controller';
import { MenuButtonController } from './components-lib/menu-button-controller';
import './components/elements.js';
import { FrigateCardElements } from './components/elements.js';
import './components/loading.js';
import './components/menu.js';
import { FrigateCardMenu } from './components/menu.js';
import './components/message.js';
@@ -183,7 +184,6 @@ class FrigateCard extends LitElement {
if (!this._controller.getInitializationManager().isInitializedMandatory()) {
this._controller.getInitializationManager().initializeMandatory();
return false;
}
return true;
}
@@ -338,6 +338,11 @@ class FrigateCard extends LitElement {
const actions = this._controller.getActionsManager().getMergedActions();
const cameraManager = this._controller.getCameraManager();
const renderLoadingSpinner =
this._config?.performance?.features.animated_progress_indicator !== false;
const showLoadingSpinner =
!this._controller.getInitializationManager().wasEverInitialized() &&
!this._controller.getMessageManager().hasMessage();
// Caution: Keep the main div and the menu next to one another in order to
// ensure the hover menu styling continues to work.
@@ -366,35 +371,35 @@ class FrigateCard extends LitElement {
}
@frigate-card:focus=${() => this.focus()}
>
${renderLoadingSpinner
? html`<frigate-card-loading .show=${showLoadingSpinner}>
</frigate-card-loading>`
: ''}
${this._renderMenuStatusContainer('top')}
<div ${ref(this._refMain)} class="${classMap(mainClasses)}">
${this._renderMenuStatusContainer('overlay')}
${
// Always want to render <frigate-card-views> even if there's a message, to
// ensure live preload is always present (even if not displayed).
html`<frigate-card-views
${ref(this._refViews)}
.hass=${this._hass}
.viewManagerEpoch=${this._controller.getViewManager().getEpoch()}
.cameraManager=${cameraManager}
.resolvedMediaCache=${this._controller.getResolvedMediaCache()}
.nonOverriddenConfig=${this._controller
.getConfigManager()
.getNonOverriddenConfig()}
.overriddenConfig=${this._controller.getConfigManager().getConfig()}
.cardWideConfig=${this._controller.getConfigManager().getCardWideConfig()}
.rawConfig=${this._controller.getConfigManager().getRawConfig()}
.configManager=${this._controller.getConfigManager()}
.conditionsManagerEpoch=${this._controller
.getConditionsManager()
?.getEpoch()}
.hide=${!!this._controller.getMessageManager().hasMessage()}
.microphoneManager=${this._controller.getMicrophoneManager()}
.triggeredCameraIDs=${this._config?.view.triggers.show_trigger_status
? this._controller.getTriggersManager().getTriggeredCameraIDs()
: undefined}
></frigate-card-views>`
}
<frigate-card-views
${ref(this._refViews)}
.hass=${this._hass}
.viewManagerEpoch=${this._controller.getViewManager().getEpoch()}
.cameraManager=${cameraManager}
.resolvedMediaCache=${this._controller.getResolvedMediaCache()}
.nonOverriddenConfig=${this._controller
.getConfigManager()
.getNonOverriddenConfig()}
.overriddenConfig=${this._controller.getConfigManager().getConfig()}
.cardWideConfig=${this._controller.getConfigManager().getCardWideConfig()}
.rawConfig=${this._controller.getConfigManager().getRawConfig()}
.configManager=${this._controller.getConfigManager()}
.conditionsManagerEpoch=${this._controller
.getConditionsManager()
?.getEpoch()}
.hide=${!!this._controller.getMessageManager().hasMessage()}
.microphoneManager=${this._controller.getMicrophoneManager()}
.triggeredCameraIDs=${this._config?.view.triggers.show_trigger_status
? this._controller.getTriggersManager().getTriggeredCameraIDs()
: undefined}
></frigate-card-views>
${
// Keep message rendering to last to show messages that may have been
// generated during the render.
+49
View File
@@ -0,0 +1,49 @@
import {
CSSResultGroup,
LitElement,
PropertyValues,
TemplateResult,
html,
unsafeCSS,
} from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import irisLogo from '../images/camera-iris.svg';
import controlStyle from '../scss/loading.scss';
import { Timer } from '../utils/timer';
// Number of seconds after the loading spinner is hidden before rendering this
// component as empty. Should be longer than the opacity css transition time.
const LOADING_EMPTY_SECONDS = 2;
@customElement('frigate-card-loading')
export class FrigateCardLoading extends LitElement {
@property({ attribute: true, reflect: true, type: Boolean })
public show = false;
@state()
protected _empty = false;
protected _timer = new Timer();
protected render(): TemplateResult {
return this._empty ? html`` : html` <img src="${irisLogo}" /> `;
}
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('show') && !this.show) {
this._timer.start(LOADING_EMPTY_SECONDS, () => {
this._empty = true;
});
}
}
static get styles(): CSSResultGroup {
return unsafeCSS(controlStyle);
}
}
declare global {
interface HTMLElementTagNameMap {
'frigate-card-loading': FrigateCardLoading;
}
}
+7
View File
@@ -13,6 +13,7 @@
border-radius: var(--ha-card-border-radius, 4px);
height: var(--frigate-card-height);
min-height: 100px;
// Ensure all clicks at the top level work.
pointer-events: all;
@@ -29,6 +30,12 @@
--frigate-card-height: auto;
}
frigate-card-loading {
position: absolute;
inset: 0;
z-index: 1;
}
:host([dark]) {
filter: brightness(75%);
}
+40
View File
@@ -0,0 +1,40 @@
:host {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
transition: opacity 1s;
}
:host([show]) {
opacity: 1;
}
:host(:not([show])) {
opacity: 0;
}
img {
width: 40%;
height: 40%;
opacity: 0.2;
filter: invert(100%);
animation: rotate 8s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}