diff --git a/package.json b/package.json index 0313b1fa..50ff7ff0 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@babel/plugin-proposal-class-properties": "^7.14.5", "@babel/plugin-proposal-decorators": "^7.15.4", "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-multi-entry": "^4.1.0", "@typescript-eslint/eslint-plugin": "^4.30.0", "@typescript-eslint/parser": "^4.30.0", "eslint": "^7.32.0", diff --git a/rollup.config.js b/rollup.config.js index dc2c7e3a..ab52b946 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -6,6 +6,7 @@ import { terser } from 'rollup-plugin-terser'; import serve from 'rollup-plugin-serve'; import json from '@rollup/plugin-json'; import styles from 'rollup-plugin-styles'; +import multi from '@rollup/plugin-multi-entry'; const dev = process.env.ROLLUP_WATCH; @@ -20,6 +21,7 @@ const serveopts = { }; const plugins = [ + multi(), styles({ modules: false, // Behavior of inject mode, without actually injecting style @@ -42,9 +44,9 @@ const plugins = [ export default [ { - input: 'src/frigate-hass-card.ts', + input: ['src/main.ts'], output: { - dir: 'dist', + file: 'dist/frigate-hass-card.js', format: 'es', }, plugins: [...plugins], diff --git a/src/frigate-hass-card.ts b/src/main.ts similarity index 95% rename from src/frigate-hass-card.ts rename to src/main.ts index 8a30d99a..bfffab27 100644 --- a/src/frigate-hass-card.ts +++ b/src/main.ts @@ -11,6 +11,8 @@ import { customElement, property, query, state } from 'lit/decorators'; import { classMap } from 'lit/directives/class-map.js'; import { until } from 'lit/directives/until.js'; +import { renderMessage, renderErrorMessage, renderProgressIndicator } from './message'; + import { HomeAssistant, LovelaceCardEditor, @@ -50,9 +52,6 @@ import { MessageBase } from 'home-assistant-js-websocket'; import JSMpeg from '@cycjimmy/jsmpeg-player'; -const URL_TROUBLESHOOTING = - 'https://github.com/dermotduffy/frigate-hass-card#troubleshooting'; - // Load dayjs plugin(s). dayjs.extend(dayjs_custom_parse_format); @@ -520,28 +519,6 @@ export class FrigateCard extends LitElement { return this._makeWSRequest(resolvedMediaSchema, request); } - // Render an attention grabbing icon. - protected _renderAttentionIcon( - icon: string, - message: string | TemplateResult | null = null, - ): TemplateResult { - return html`
- - - ${message ? html` ${message}` : ''} - -
`; - } - - // Render an embedded error situation. - protected _renderError(error: string): TemplateResult { - return this._renderAttentionIcon( - 'mdi:alert-circle', - html`${error ? `${error}.` : `${localize('error.unknown_error')}.`} - ${localize('error.troubleshooting')}.`, - ); - } - // Render Frigate events into a card gallery. protected async _renderEvents(): Promise { let parent; @@ -552,15 +529,15 @@ export class FrigateCard extends LitElement { parent = await this._browseMediaQuery(this._view.is('clips')); } } catch (e: any) { - return this._renderError(e.message); + return renderErrorMessage(e.message); } if (this._getFirstTrueMediaChildIndex(parent) == null) { - return this._renderAttentionIcon( - this._view.is('clips') ? 'mdi:filmstrip-off' : 'mdi:camera-off', + return renderMessage( this._view.is('clips') - ? localize('common.no_clips') - : localize('common.no_snapshots'), + ? localize('common.no_clips') + : localize('common.no_snapshots'), + this._view.is('clips') ? 'mdi:filmstrip-off' : 'mdi:camera-off', ); } @@ -625,13 +602,6 @@ export class FrigateCard extends LitElement { `; } - // Render a progress spinner while content loads. - protected _renderProgressIndicator(): TemplateResult { - return html`
- -
`; - } - protected _menuActionHandler(name: string): void { switch (name) { case 'frigate': @@ -802,15 +772,15 @@ export class FrigateCard extends LitElement { try { parent = await this._browseMediaQuery(this._view.is('clip')); } catch (e: any) { - return this._renderError(e.message); + return renderErrorMessage(e.message); } childIndex = this._getFirstTrueMediaChildIndex(parent); if (!parent || !parent.children || childIndex == null) { - return this._renderAttentionIcon( - this._view.is('clip') ? 'mdi:filmstrip-off' : 'mdi:camera-off', + return renderMessage( this._view.is('clip') ? localize('common.no_clip') : localize('common.no_snapshot'), + this._view.is('clip') ? 'mdi:filmstrip-off' : 'mdi:camera-off', ); } mediaToRender = parent.children[childIndex]; @@ -825,7 +795,7 @@ export class FrigateCard extends LitElement { const resolvedMedia = await this._resolveMedia(mediaToRender); if (!mediaToRender || !resolvedMedia) { // Home Assistant could not resolve media item. - return this._renderError(localize('error.could_not_resolve')); + return renderErrorMessage(localize('error.could_not_resolve')); } const neighbors = this._getMediaNeighbors(parent, childIndex); @@ -998,7 +968,7 @@ export class FrigateCard extends LitElement { const jsmpeg_url = await this._getJSMPEGURL(); if (!jsmpeg_url) { - return this._renderError('Could not retrieve or sign JSMPEG websocket path'); + return renderErrorMessage('Could not retrieve or sign JSMPEG websocket path'); } // Return the html canvas node only after the JSMPEG video has loaded and @@ -1029,9 +999,9 @@ export class FrigateCard extends LitElement { // is always rendered (but sometimes hidden). protected async _renderLiveViewer(): Promise { if (!this._hass || !(this.config.camera_entity in this._hass.states)) { - return this._renderAttentionIcon( - 'mdi:camera-off', + return renderMessage( localize('error.no_live_camera'), + 'mdi:camera-off', ); } if (this._webrtcElement) { @@ -1091,13 +1061,13 @@ export class FrigateCard extends LitElement {
${this._view.is('clips') || this._view.is('snapshots') - ? until(this._renderEvents(), this._renderProgressIndicator()) + ? until(this._renderEvents(), renderProgressIndicator()) : ``} ${this._view.is('clip') || this._view.is('snapshot') - ? until(this._renderViewer(), this._renderProgressIndicator()) + ? until(this._renderViewer(), renderProgressIndicator()) : ``} ${this._view.is('live') - ? until(this._renderLiveViewer(), this._renderProgressIndicator()) + ? until(this._renderLiveViewer(), renderProgressIndicator()) : ``}
diff --git a/src/message.ts b/src/message.ts new file mode 100644 index 00000000..34ddd999 --- /dev/null +++ b/src/message.ts @@ -0,0 +1,75 @@ +import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators'; +import { localize } from './localize/localize'; + +import frigate_card_message_style from './scss/message.scss'; + +const URL_TROUBLESHOOTING = + 'https://github.com/dermotduffy/frigate-hass-card#troubleshooting'; + +@customElement('frigate-card-message') +export class FrigateCardMessage extends LitElement { + @property({ attribute: false }) + protected message = ''; + + @property({ attribute: false }) + protected icon = 'mdi:information-outline'; + + // Render the menu. + protected render(): TemplateResult { + return html`
+ + + ${this.message ? html` ${this.message}` : ''} + +
`; + } + + static get styles(): CSSResultGroup { + return unsafeCSS(frigate_card_message_style); + } +} + +@customElement('frigate-card-error-message') +export class FrigateCardErrorMessage extends LitElement { + @property({ attribute: false }) + protected error = ''; + + protected render(): TemplateResult { + return html` ${localize('error.troubleshooting')} .`} + .icon=${'mdi:alert-circle'} + > + `; + } +} + +@customElement('frigate-card-progress-indicator') +export class FrigateCardProgressIndicator extends LitElement { + protected render(): TemplateResult { + return html`
+ +
`; + } + + static get styles(): CSSResultGroup { + return unsafeCSS(frigate_card_message_style); + } +} + +export function renderErrorMessage(error: string): TemplateResult { + return html` + + `; +} + +export function renderMessage(message: string, icon: string): TemplateResult { + return html` + + `; +} + +export function renderProgressIndicator(): TemplateResult { + return html` `; +} diff --git a/src/scss/message.scss b/src/scss/message.scss new file mode 100644 index 00000000..8752733f --- /dev/null +++ b/src/scss/message.scss @@ -0,0 +1,8 @@ +.message { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + padding: 10%; +} \ No newline at end of file