diff --git a/src/dashboards/hacs-dashboard.ts b/src/dashboards/hacs-dashboard.ts index a42c4bb4..c8f94a5b 100644 --- a/src/dashboards/hacs-dashboard.ts +++ b/src/dashboards/hacs-dashboard.ts @@ -10,7 +10,7 @@ import { mdiInformation, mdiNewBox, } from "@mdi/js"; -import type { CSSResultGroup, TemplateResult } from "lit"; +import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { LitElement, html, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoize from "memoize-one"; @@ -63,6 +63,7 @@ const defaultKeyData = { }; const STATUS_ORDER = ["pending-restart", "pending-upgrade", "installed", "new", "default"]; +const BRANDS_TOKEN_REFRESH_INTERVAL = 30 * 60 * 1000; const TABS: PageNavigation[] = [ { @@ -114,6 +115,28 @@ export class HacsDashboard extends LitElement { @state() private _overflowMenuRepository?: RepositoryBase; + @state() + private _brandsAccessToken?: string; + + private _brandsTokenRefreshInterval?: number; + + public connectedCallback(): void { + super.connectedCallback(); + if (this.hasUpdated) { + this._startBrandsTokenRefresh(); + } + } + + public disconnectedCallback(): void { + super.disconnectedCallback(); + this._stopBrandsTokenRefresh(); + } + + protected firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + this._startBrandsTokenRefresh(); + } + protected render = (): TemplateResult | void => { const repositories = this._filterRepositories( this.hacs.repositories, @@ -327,13 +350,20 @@ export class HacsDashboard extends LitElement { + this._handleRepositoryIconError(event, repository.domain)} /> ` : html` @@ -581,6 +611,46 @@ export class HacsDashboard extends LitElement { this._activeFilters = undefined; } + private _startBrandsTokenRefresh(): void { + this._stopBrandsTokenRefresh(); + void this._fetchBrandsAccessToken(); + this._brandsTokenRefreshInterval = window.setInterval( + () => void this._fetchBrandsAccessToken(), + BRANDS_TOKEN_REFRESH_INTERVAL, + ); + } + + private _stopBrandsTokenRefresh(): void { + if (this._brandsTokenRefreshInterval !== undefined) { + window.clearInterval(this._brandsTokenRefreshInterval); + this._brandsTokenRefreshInterval = undefined; + } + } + + private async _fetchBrandsAccessToken(): Promise { + try { + const { token } = await this.hass.callWS<{ token: string }>({ + type: "brands/access_token", + }); + this._brandsAccessToken = token; + } catch { + this._brandsAccessToken = undefined; + } + } + + private _handleRepositoryIconError(event: Event, domain?: string): void { + const image = event.currentTarget as HTMLImageElement; + const fallback = brandsUrl({ + domain: domain || "invalid", + type: "icon", + useFallback: true, + darkOptimized: this.hass.themes?.darkMode, + }); + if (image.src !== fallback) { + image.src = fallback; + } + } + static get styles(): CSSResultGroup { return [haStyle, HacsStyles]; }