mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-30 22:19:55 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			more-info-
			...
			auth-passw
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | bd316a36a0 | 
| @@ -1,13 +1,12 @@ | |||||||
| import "@material/mwc-icon-button/mwc-icon-button"; | import "@material/mwc-icon-button/mwc-icon-button"; | ||||||
| import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; |  | ||||||
| import "@material/mwc-list/mwc-list-item"; | import "@material/mwc-list/mwc-list-item"; | ||||||
| import { mdiDotsVertical } from "@mdi/js"; | import { mdiDotsVertical } from "@mdi/js"; | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResult, |   CSSResult, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   PropertyValues, |   PropertyValues, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { html, TemplateResult } from "lit-html"; | import { html, TemplateResult } from "lit-html"; | ||||||
| @@ -20,13 +19,13 @@ import { | |||||||
|   HassioAddonRepository, |   HassioAddonRepository, | ||||||
|   reloadHassioAddons, |   reloadHassioAddons, | ||||||
| } from "../../../src/data/hassio/addon"; | } from "../../../src/data/hassio/addon"; | ||||||
| import { extractApiErrorMessage } from "../../../src/data/hassio/common"; |  | ||||||
| import "../../../src/layouts/hass-loading-screen"; |  | ||||||
| import "../../../src/layouts/hass-tabs-subpage"; | import "../../../src/layouts/hass-tabs-subpage"; | ||||||
|  | import "../../../src/layouts/hass-loading-screen"; | ||||||
| import { HomeAssistant, Route } from "../../../src/types"; | import { HomeAssistant, Route } from "../../../src/types"; | ||||||
| import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories"; | import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories"; | ||||||
| import { supervisorTabs } from "../hassio-tabs"; | import { supervisorTabs } from "../hassio-tabs"; | ||||||
| import "./hassio-addon-repository"; | import "./hassio-addon-repository"; | ||||||
|  | import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; | ||||||
|  |  | ||||||
| const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => { | const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => { | ||||||
|   if (a.slug === "local") { |   if (a.slug === "local") { | ||||||
| @@ -180,7 +179,7 @@ class HassioAddonStore extends LitElement { | |||||||
|       this._repos.sort(sortRepos); |       this._repos.sort(sortRepos); | ||||||
|       this._addons = addonsInfo.addons; |       this._addons = addonsInfo.addons; | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       alert(extractApiErrorMessage(err)); |       alert("Failed to fetch add-on info"); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -28,7 +28,6 @@ import { haStyle } from "../../../../src/resources/styles"; | |||||||
| import { HomeAssistant } from "../../../../src/types"; | import { HomeAssistant } from "../../../../src/types"; | ||||||
| import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart"; | import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart"; | ||||||
| import { hassioStyle } from "../../resources/hassio-style"; | import { hassioStyle } from "../../resources/hassio-style"; | ||||||
| import "../../../../src/components/buttons/ha-progress-button"; |  | ||||||
|  |  | ||||||
| @customElement("hassio-addon-audio") | @customElement("hassio-addon-audio") | ||||||
| class HassioAddonAudio extends LitElement { | class HassioAddonAudio extends LitElement { | ||||||
| @@ -92,9 +91,7 @@ class HassioAddonAudio extends LitElement { | |||||||
|           </paper-dropdown-menu> |           </paper-dropdown-menu> | ||||||
|         </div> |         </div> | ||||||
|         <div class="card-actions"> |         <div class="card-actions"> | ||||||
|           <ha-progress-button @click=${this._saveSettings}> |           <mwc-button @click=${this._saveSettings}>Save</mwc-button> | ||||||
|             Save |  | ||||||
|           </ha-progress-button> |  | ||||||
|         </div> |         </div> | ||||||
|       </ha-card> |       </ha-card> | ||||||
|     `; |     `; | ||||||
| @@ -175,10 +172,7 @@ class HassioAddonAudio extends LitElement { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _saveSettings(ev: CustomEvent): Promise<void> { |   private async _saveSettings(): Promise<void> { | ||||||
|     const button = ev.currentTarget as any; |  | ||||||
|     button.progress = true; |  | ||||||
|  |  | ||||||
|     this._error = undefined; |     this._error = undefined; | ||||||
|     const data: HassioAddonSetOptionParams = { |     const data: HassioAddonSetOptionParams = { | ||||||
|       audio_input: |       audio_input: | ||||||
| @@ -188,14 +182,12 @@ class HassioAddonAudio extends LitElement { | |||||||
|     }; |     }; | ||||||
|     try { |     try { | ||||||
|       await setHassioAddonOption(this.hass, this.addon.slug, data); |       await setHassioAddonOption(this.hass, this.addon.slug, data); | ||||||
|       if (this.addon?.state === "started") { |  | ||||||
|         await suggestAddonRestart(this, this.hass, this.addon); |  | ||||||
|       } |  | ||||||
|     } catch { |     } catch { | ||||||
|       this._error = "Failed to set addon audio device"; |       this._error = "Failed to set addon audio device"; | ||||||
|     } |     } | ||||||
|  |     if (!this._error && this.addon?.state === "started") { | ||||||
|     button.progress = false; |       await suggestAddonRestart(this, this.hass, this.addon); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,15 +5,14 @@ import { | |||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   PropertyValues, |   PropertyValues, | ||||||
|   query, |   query, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { fireEvent } from "../../../../src/common/dom/fire_event"; | import { fireEvent } from "../../../../src/common/dom/fire_event"; | ||||||
| import "../../../../src/components/buttons/ha-progress-button"; |  | ||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
| import "../../../../src/components/ha-yaml-editor"; | import "../../../../src/components/ha-yaml-editor"; | ||||||
| import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor"; | import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor"; | ||||||
| @@ -22,7 +21,6 @@ import { | |||||||
|   HassioAddonSetOptionParams, |   HassioAddonSetOptionParams, | ||||||
|   setHassioAddonOption, |   setHassioAddonOption, | ||||||
| } from "../../../../src/data/hassio/addon"; | } from "../../../../src/data/hassio/addon"; | ||||||
| import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; |  | ||||||
| import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box"; | import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box"; | ||||||
| import { haStyle } from "../../../../src/resources/styles"; | import { haStyle } from "../../../../src/resources/styles"; | ||||||
| import type { HomeAssistant } from "../../../../src/types"; | import type { HomeAssistant } from "../../../../src/types"; | ||||||
| @@ -57,103 +55,20 @@ class HassioAddonConfig extends LitElement { | |||||||
|           ${valid ? "" : html` <div class="errors">Invalid YAML</div> `} |           ${valid ? "" : html` <div class="errors">Invalid YAML</div> `} | ||||||
|         </div> |         </div> | ||||||
|         <div class="card-actions"> |         <div class="card-actions"> | ||||||
|           <ha-progress-button class="warning" @click=${this._resetTapped}> |           <mwc-button class="warning" @click=${this._resetTapped}> | ||||||
|             Reset to defaults |             Reset to defaults | ||||||
|           </ha-progress-button> |           </mwc-button> | ||||||
|           <ha-progress-button |           <mwc-button | ||||||
|             @click=${this._saveTapped} |             @click=${this._saveTapped} | ||||||
|             .disabled=${!this._configHasChanged || !valid} |             .disabled=${!this._configHasChanged || !valid} | ||||||
|           > |           > | ||||||
|             Save |             Save | ||||||
|           </ha-progress-button> |           </mwc-button> | ||||||
|         </div> |         </div> | ||||||
|       </ha-card> |       </ha-card> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected updated(changedProperties: PropertyValues): void { |  | ||||||
|     super.updated(changedProperties); |  | ||||||
|     if (changedProperties.has("addon")) { |  | ||||||
|       this._editor.setValue(this.addon.options); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _configChanged(): void { |  | ||||||
|     this._configHasChanged = true; |  | ||||||
|     this.requestUpdate(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _resetTapped(ev: CustomEvent): Promise<void> { |  | ||||||
|     const button = ev.currentTarget as any; |  | ||||||
|     button.progress = true; |  | ||||||
|  |  | ||||||
|     const confirmed = await showConfirmationDialog(this, { |  | ||||||
|       title: this.addon.name, |  | ||||||
|       text: "Are you sure you want to reset all your options?", |  | ||||||
|       confirmText: "reset options", |  | ||||||
|       dismissText: "no", |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     if (!confirmed) { |  | ||||||
|       button.progress = false; |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this._error = undefined; |  | ||||||
|     const data: HassioAddonSetOptionParams = { |  | ||||||
|       options: null, |  | ||||||
|     }; |  | ||||||
|     try { |  | ||||||
|       await setHassioAddonOption(this.hass, this.addon.slug, data); |  | ||||||
|       this._configHasChanged = false; |  | ||||||
|       const eventdata = { |  | ||||||
|         success: true, |  | ||||||
|         response: undefined, |  | ||||||
|         path: "options", |  | ||||||
|       }; |  | ||||||
|       fireEvent(this, "hass-api-called", eventdata); |  | ||||||
|     } catch (err) { |  | ||||||
|       this._error = `Failed to reset addon configuration, ${extractApiErrorMessage( |  | ||||||
|         err |  | ||||||
|       )}`; |  | ||||||
|     } |  | ||||||
|     button.progress = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _saveTapped(ev: CustomEvent): Promise<void> { |  | ||||||
|     const button = ev.currentTarget as any; |  | ||||||
|     button.progress = true; |  | ||||||
|  |  | ||||||
|     let data: HassioAddonSetOptionParams; |  | ||||||
|     this._error = undefined; |  | ||||||
|     try { |  | ||||||
|       data = { |  | ||||||
|         options: this._editor.value, |  | ||||||
|       }; |  | ||||||
|     } catch (err) { |  | ||||||
|       this._error = err; |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     try { |  | ||||||
|       await setHassioAddonOption(this.hass, this.addon.slug, data); |  | ||||||
|       this._configHasChanged = false; |  | ||||||
|       const eventdata = { |  | ||||||
|         success: true, |  | ||||||
|         response: undefined, |  | ||||||
|         path: "options", |  | ||||||
|       }; |  | ||||||
|       fireEvent(this, "hass-api-called", eventdata); |  | ||||||
|       if (this.addon?.state === "started") { |  | ||||||
|         await suggestAddonRestart(this, this.hass, this.addon); |  | ||||||
|       } |  | ||||||
|     } catch (err) { |  | ||||||
|       this._error = `Failed to save addon configuration, ${extractApiErrorMessage( |  | ||||||
|         err |  | ||||||
|       )}`; |  | ||||||
|     } |  | ||||||
|     button.progress = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult[] { |   static get styles(): CSSResult[] { | ||||||
|     return [ |     return [ | ||||||
|       haStyle, |       haStyle, | ||||||
| @@ -183,6 +98,80 @@ class HassioAddonConfig extends LitElement { | |||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   protected updated(changedProperties: PropertyValues): void { | ||||||
|  |     super.updated(changedProperties); | ||||||
|  |     if (changedProperties.has("addon")) { | ||||||
|  |       this._editor.setValue(this.addon.options); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _configChanged(): void { | ||||||
|  |     this._configHasChanged = true; | ||||||
|  |     this.requestUpdate(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _resetTapped(): Promise<void> { | ||||||
|  |     const confirmed = await showConfirmationDialog(this, { | ||||||
|  |       title: this.addon.name, | ||||||
|  |       text: "Are you sure you want to reset all your options?", | ||||||
|  |       confirmText: "reset options", | ||||||
|  |       dismissText: "no", | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if (!confirmed) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this._error = undefined; | ||||||
|  |     const data: HassioAddonSetOptionParams = { | ||||||
|  |       options: null, | ||||||
|  |     }; | ||||||
|  |     try { | ||||||
|  |       await setHassioAddonOption(this.hass, this.addon.slug, data); | ||||||
|  |       this._configHasChanged = false; | ||||||
|  |       const eventdata = { | ||||||
|  |         success: true, | ||||||
|  |         response: undefined, | ||||||
|  |         path: "options", | ||||||
|  |       }; | ||||||
|  |       fireEvent(this, "hass-api-called", eventdata); | ||||||
|  |     } catch (err) { | ||||||
|  |       this._error = `Failed to reset addon configuration, ${ | ||||||
|  |         err.body?.message || err | ||||||
|  |       }`; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _saveTapped(): Promise<void> { | ||||||
|  |     let data: HassioAddonSetOptionParams; | ||||||
|  |     this._error = undefined; | ||||||
|  |     try { | ||||||
|  |       data = { | ||||||
|  |         options: this._editor.value, | ||||||
|  |       }; | ||||||
|  |     } catch (err) { | ||||||
|  |       this._error = err; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     try { | ||||||
|  |       await setHassioAddonOption(this.hass, this.addon.slug, data); | ||||||
|  |       this._configHasChanged = false; | ||||||
|  |       const eventdata = { | ||||||
|  |         success: true, | ||||||
|  |         response: undefined, | ||||||
|  |         path: "options", | ||||||
|  |       }; | ||||||
|  |       fireEvent(this, "hass-api-called", eventdata); | ||||||
|  |     } catch (err) { | ||||||
|  |       this._error = `Failed to save addon configuration, ${ | ||||||
|  |         err.body?.message || err | ||||||
|  |       }`; | ||||||
|  |     } | ||||||
|  |     if (!this._error && this.addon?.state === "started") { | ||||||
|  |       await suggestAddonRestart(this, this.hass, this.addon); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -4,21 +4,19 @@ import { | |||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   PropertyValues, |   PropertyValues, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { fireEvent } from "../../../../src/common/dom/fire_event"; | import { fireEvent } from "../../../../src/common/dom/fire_event"; | ||||||
| import "../../../../src/components/buttons/ha-progress-button"; |  | ||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
| import { | import { | ||||||
|   HassioAddonDetails, |   HassioAddonDetails, | ||||||
|   HassioAddonSetOptionParams, |   HassioAddonSetOptionParams, | ||||||
|   setHassioAddonOption, |   setHassioAddonOption, | ||||||
| } from "../../../../src/data/hassio/addon"; | } from "../../../../src/data/hassio/addon"; | ||||||
| import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; |  | ||||||
| import { haStyle } from "../../../../src/resources/styles"; | import { haStyle } from "../../../../src/resources/styles"; | ||||||
| import { HomeAssistant } from "../../../../src/types"; | import { HomeAssistant } from "../../../../src/types"; | ||||||
| import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart"; | import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart"; | ||||||
| @@ -87,17 +85,38 @@ class HassioAddonNetwork extends LitElement { | |||||||
|           </table> |           </table> | ||||||
|         </div> |         </div> | ||||||
|         <div class="card-actions"> |         <div class="card-actions"> | ||||||
|           <ha-progress-button class="warning" @click=${this._resetTapped}> |           <mwc-button class="warning" @click=${this._resetTapped}> | ||||||
|             Reset to defaults |             Reset to defaults | ||||||
|           </ha-progress-button> |           </mwc-button> | ||||||
|           <ha-progress-button @click=${this._saveTapped}> |           <mwc-button @click=${this._saveTapped}>Save</mwc-button> | ||||||
|             Save |  | ||||||
|           </ha-progress-button> |  | ||||||
|         </div> |         </div> | ||||||
|       </ha-card> |       </ha-card> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   static get styles(): CSSResult[] { | ||||||
|  |     return [ | ||||||
|  |       haStyle, | ||||||
|  |       hassioStyle, | ||||||
|  |       css` | ||||||
|  |         :host { | ||||||
|  |           display: block; | ||||||
|  |         } | ||||||
|  |         ha-card { | ||||||
|  |           display: block; | ||||||
|  |         } | ||||||
|  |         .errors { | ||||||
|  |           color: var(--error-color); | ||||||
|  |           margin-bottom: 16px; | ||||||
|  |         } | ||||||
|  |         .card-actions { | ||||||
|  |           display: flex; | ||||||
|  |           justify-content: space-between; | ||||||
|  |         } | ||||||
|  |       `, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   protected update(changedProperties: PropertyValues): void { |   protected update(changedProperties: PropertyValues): void { | ||||||
|     super.update(changedProperties); |     super.update(changedProperties); | ||||||
|     if (changedProperties.has("addon")) { |     if (changedProperties.has("addon")) { | ||||||
| @@ -130,10 +149,7 @@ class HassioAddonNetwork extends LitElement { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _resetTapped(ev: CustomEvent): Promise<void> { |   private async _resetTapped(): Promise<void> { | ||||||
|     const button = ev.currentTarget as any; |  | ||||||
|     button.progress = true; |  | ||||||
|  |  | ||||||
|     const data: HassioAddonSetOptionParams = { |     const data: HassioAddonSetOptionParams = { | ||||||
|       network: null, |       network: null, | ||||||
|     }; |     }; | ||||||
| @@ -146,22 +162,17 @@ class HassioAddonNetwork extends LitElement { | |||||||
|         path: "option", |         path: "option", | ||||||
|       }; |       }; | ||||||
|       fireEvent(this, "hass-api-called", eventdata); |       fireEvent(this, "hass-api-called", eventdata); | ||||||
|       if (this.addon?.state === "started") { |     } catch (err) { | ||||||
|  |       this._error = `Failed to set addon network configuration, ${ | ||||||
|  |         err.body?.message || err | ||||||
|  |       }`; | ||||||
|  |     } | ||||||
|  |     if (!this._error && this.addon?.state === "started") { | ||||||
|       await suggestAddonRestart(this, this.hass, this.addon); |       await suggestAddonRestart(this, this.hass, this.addon); | ||||||
|     } |     } | ||||||
|     } catch (err) { |  | ||||||
|       this._error = `Failed to set addon network configuration, ${extractApiErrorMessage( |  | ||||||
|         err |  | ||||||
|       )}`; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|     button.progress = false; |   private async _saveTapped(): Promise<void> { | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _saveTapped(ev: CustomEvent): Promise<void> { |  | ||||||
|     const button = ev.currentTarget as any; |  | ||||||
|     button.progress = true; |  | ||||||
|  |  | ||||||
|     this._error = undefined; |     this._error = undefined; | ||||||
|     const networkconfiguration = {}; |     const networkconfiguration = {}; | ||||||
|     this._config!.forEach((item) => { |     this._config!.forEach((item) => { | ||||||
| @@ -180,38 +191,14 @@ class HassioAddonNetwork extends LitElement { | |||||||
|         path: "option", |         path: "option", | ||||||
|       }; |       }; | ||||||
|       fireEvent(this, "hass-api-called", eventdata); |       fireEvent(this, "hass-api-called", eventdata); | ||||||
|       if (this.addon?.state === "started") { |     } catch (err) { | ||||||
|  |       this._error = `Failed to set addon network configuration, ${ | ||||||
|  |         err.body?.message || err | ||||||
|  |       }`; | ||||||
|  |     } | ||||||
|  |     if (!this._error && this.addon?.state === "started") { | ||||||
|       await suggestAddonRestart(this, this.hass, this.addon); |       await suggestAddonRestart(this, this.hass, this.addon); | ||||||
|     } |     } | ||||||
|     } catch (err) { |  | ||||||
|       this._error = `Failed to set addon network configuration, ${extractApiErrorMessage( |  | ||||||
|         err |  | ||||||
|       )}`; |  | ||||||
|     } |  | ||||||
|     button.progress = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult[] { |  | ||||||
|     return [ |  | ||||||
|       haStyle, |  | ||||||
|       hassioStyle, |  | ||||||
|       css` |  | ||||||
|         :host { |  | ||||||
|           display: block; |  | ||||||
|         } |  | ||||||
|         ha-card { |  | ||||||
|           display: block; |  | ||||||
|         } |  | ||||||
|         .errors { |  | ||||||
|           color: var(--error-color); |  | ||||||
|           margin-bottom: 16px; |  | ||||||
|         } |  | ||||||
|         .card-actions { |  | ||||||
|           display: flex; |  | ||||||
|           justify-content: space-between; |  | ||||||
|         } |  | ||||||
|       `, |  | ||||||
|     ]; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,19 +3,18 @@ import { | |||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import "../../../../src/components/ha-circular-progress"; |  | ||||||
| import "../../../../src/components/ha-markdown"; | import "../../../../src/components/ha-markdown"; | ||||||
| import { | import { | ||||||
|   fetchHassioAddonDocumentation, |   fetchHassioAddonDocumentation, | ||||||
|   HassioAddonDetails, |   HassioAddonDetails, | ||||||
| } from "../../../../src/data/hassio/addon"; | } from "../../../../src/data/hassio/addon"; | ||||||
| import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; |  | ||||||
| import "../../../../src/layouts/hass-loading-screen"; | import "../../../../src/layouts/hass-loading-screen"; | ||||||
|  | import "../../../../src/components/ha-circular-progress"; | ||||||
| import { haStyle } from "../../../../src/resources/styles"; | import { haStyle } from "../../../../src/resources/styles"; | ||||||
| import { HomeAssistant } from "../../../../src/types"; | import { HomeAssistant } from "../../../../src/types"; | ||||||
| import { hassioStyle } from "../../resources/hassio-style"; | import { hassioStyle } from "../../resources/hassio-style"; | ||||||
| @@ -81,9 +80,9 @@ class HassioAddonDocumentationDashboard extends LitElement { | |||||||
|         this.addon!.slug |         this.addon!.slug | ||||||
|       ); |       ); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       this._error = `Failed to get addon documentation, ${extractApiErrorMessage( |       this._error = `Failed to get addon documentation, ${ | ||||||
|         err |         err.body?.message || err | ||||||
|       )}`; |       }`; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,19 +9,21 @@ import { | |||||||
|   mdiExclamationThick, |   mdiExclamationThick, | ||||||
|   mdiFlask, |   mdiFlask, | ||||||
|   mdiHomeAssistant, |   mdiHomeAssistant, | ||||||
|  |   mdiInformation, | ||||||
|   mdiKey, |   mdiKey, | ||||||
|   mdiNetwork, |   mdiNetwork, | ||||||
|   mdiPound, |   mdiPound, | ||||||
|   mdiShield, |   mdiShield, | ||||||
| } from "@mdi/js"; | } from "@mdi/js"; | ||||||
|  | import "@polymer/paper-tooltip/paper-tooltip"; | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { classMap } from "lit-html/directives/class-map"; | import { classMap } from "lit-html/directives/class-map"; | ||||||
| @@ -33,27 +35,19 @@ import "../../../../src/components/buttons/ha-progress-button"; | |||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
| import "../../../../src/components/ha-label-badge"; | import "../../../../src/components/ha-label-badge"; | ||||||
| import "../../../../src/components/ha-markdown"; | import "../../../../src/components/ha-markdown"; | ||||||
| import "../../../../src/components/ha-settings-row"; |  | ||||||
| import "../../../../src/components/ha-svg-icon"; | import "../../../../src/components/ha-svg-icon"; | ||||||
| import "../../../../src/components/ha-switch"; | import "../../../../src/components/ha-switch"; | ||||||
| import { | import { | ||||||
|   fetchHassioAddonChangelog, |   fetchHassioAddonChangelog, | ||||||
|   fetchHassioAddonInfo, |  | ||||||
|   HassioAddonDetails, |   HassioAddonDetails, | ||||||
|   HassioAddonSetOptionParams, |   HassioAddonSetOptionParams, | ||||||
|   HassioAddonSetSecurityParams, |   HassioAddonSetSecurityParams, | ||||||
|   installHassioAddon, |   installHassioAddon, | ||||||
|   setHassioAddonOption, |   setHassioAddonOption, | ||||||
|   setHassioAddonSecurity, |   setHassioAddonSecurity, | ||||||
|   startHassioAddon, |  | ||||||
|   uninstallHassioAddon, |   uninstallHassioAddon, | ||||||
|   validateHassioAddonOption, |  | ||||||
| } from "../../../../src/data/hassio/addon"; | } from "../../../../src/data/hassio/addon"; | ||||||
| import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; | import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box"; | ||||||
| import { |  | ||||||
|   showAlertDialog, |  | ||||||
|   showConfirmationDialog, |  | ||||||
| } from "../../../../src/dialogs/generic/show-dialog-box"; |  | ||||||
| import { haStyle } from "../../../../src/resources/styles"; | import { haStyle } from "../../../../src/resources/styles"; | ||||||
| import { HomeAssistant } from "../../../../src/types"; | import { HomeAssistant } from "../../../../src/types"; | ||||||
| import "../../components/hassio-card-content"; | import "../../components/hassio-card-content"; | ||||||
| @@ -133,6 +127,8 @@ class HassioAddonInfo extends LitElement { | |||||||
|  |  | ||||||
|   @internalProperty() private _error?: string; |   @internalProperty() private _error?: string; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) private _installing = false; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       ${this._computeUpdateAvailable |       ${this._computeUpdateAvailable | ||||||
| @@ -390,94 +386,67 @@ class HassioAddonInfo extends LitElement { | |||||||
|  |  | ||||||
|           ${this.addon.version |           ${this.addon.version | ||||||
|             ? html` |             ? html` | ||||||
|                 <div class="addon-options"> |                 <div class="state"> | ||||||
|                   <ha-settings-row ?three-line=${this.narrow}> |                   <div>Start on boot</div> | ||||||
|                     <span slot="heading"> |  | ||||||
|                       Start on boot |  | ||||||
|                     </span> |  | ||||||
|                     <span slot="description"> |  | ||||||
|                       Make the add-on start during a system boot |  | ||||||
|                     </span> |  | ||||||
|                   <ha-switch |                   <ha-switch | ||||||
|                     @change=${this._startOnBootToggled} |                     @change=${this._startOnBootToggled} | ||||||
|                     .checked=${this.addon.boot === "auto"} |                     .checked=${this.addon.boot === "auto"} | ||||||
|                     haptic |                     haptic | ||||||
|                   ></ha-switch> |                   ></ha-switch> | ||||||
|                   </ha-settings-row> |                 </div> | ||||||
|  |  | ||||||
|                   ${this.addon.startup !== "once" |  | ||||||
|                     ? html` |  | ||||||
|                         <ha-settings-row ?three-line=${this.narrow}> |  | ||||||
|                           <span slot="heading"> |  | ||||||
|                             Watchdog |  | ||||||
|                           </span> |  | ||||||
|                           <span slot="description"> |  | ||||||
|                             This will start the add-on if it crashes |  | ||||||
|                           </span> |  | ||||||
|                           <ha-switch |  | ||||||
|                             @change=${this._watchdogToggled} |  | ||||||
|                             .checked=${this.addon.watchdog} |  | ||||||
|                             haptic |  | ||||||
|                           ></ha-switch> |  | ||||||
|                         </ha-settings-row> |  | ||||||
|                       ` |  | ||||||
|                     : ""} |  | ||||||
|                 ${this.addon.auto_update || this.hass.userData?.showAdvanced |                 ${this.addon.auto_update || this.hass.userData?.showAdvanced | ||||||
|                   ? html` |                   ? html` | ||||||
|                         <ha-settings-row ?three-line=${this.narrow}> |                       <div class="state"> | ||||||
|                           <span slot="heading"> |                         <div>Auto update</div> | ||||||
|                             Auto update |  | ||||||
|                           </span> |  | ||||||
|                           <span slot="description"> |  | ||||||
|                             Auto update the add-on when there is a new version |  | ||||||
|                             available |  | ||||||
|                           </span> |  | ||||||
|                         <ha-switch |                         <ha-switch | ||||||
|                           @change=${this._autoUpdateToggled} |                           @change=${this._autoUpdateToggled} | ||||||
|                           .checked=${this.addon.auto_update} |                           .checked=${this.addon.auto_update} | ||||||
|                           haptic |                           haptic | ||||||
|                         ></ha-switch> |                         ></ha-switch> | ||||||
|                         </ha-settings-row> |                       </div> | ||||||
|                     ` |                     ` | ||||||
|                   : ""} |                   : ""} | ||||||
|                 ${this.addon.ingress |                 ${this.addon.ingress | ||||||
|                   ? html` |                   ? html` | ||||||
|                         <ha-settings-row ?three-line=${this.narrow}> |                       <div class="state"> | ||||||
|                           <span slot="heading"> |                         <div>Show in sidebar</div> | ||||||
|                             Show in sidebar |  | ||||||
|                           </span> |  | ||||||
|                           <span slot="description"> |  | ||||||
|                             ${this._computeCannotIngressSidebar |  | ||||||
|                               ? "This option requires Home Assistant 0.92 or later." |  | ||||||
|                               : "Add this add-on to your sidebar"} |  | ||||||
|                           </span> |  | ||||||
|                         <ha-switch |                         <ha-switch | ||||||
|                           @change=${this._panelToggled} |                           @change=${this._panelToggled} | ||||||
|                           .checked=${this.addon.ingress_panel} |                           .checked=${this.addon.ingress_panel} | ||||||
|                           .disabled=${this._computeCannotIngressSidebar} |                           .disabled=${this._computeCannotIngressSidebar} | ||||||
|                           haptic |                           haptic | ||||||
|                         ></ha-switch> |                         ></ha-switch> | ||||||
|                         </ha-settings-row> |                         ${this._computeCannotIngressSidebar | ||||||
|  |                           ? html` | ||||||
|  |                               <span> | ||||||
|  |                                 This option requires Home Assistant 0.92 or | ||||||
|  |                                 later. | ||||||
|  |                               </span> | ||||||
|  |                             ` | ||||||
|  |                           : ""} | ||||||
|  |                       </div> | ||||||
|                     ` |                     ` | ||||||
|                   : ""} |                   : ""} | ||||||
|                 ${this._computeUsesProtectedOptions |                 ${this._computeUsesProtectedOptions | ||||||
|                   ? html` |                   ? html` | ||||||
|                         <ha-settings-row ?three-line=${this.narrow}> |                       <div class="state"> | ||||||
|                           <span slot="heading"> |                         <div> | ||||||
|                           Protection mode |                           Protection mode | ||||||
|  |                           <span> | ||||||
|  |                             <ha-svg-icon path=${mdiInformation}></ha-svg-icon> | ||||||
|  |                             <paper-tooltip> | ||||||
|  |                               Grant the add-on elevated system access. | ||||||
|  |                             </paper-tooltip> | ||||||
|                           </span> |                           </span> | ||||||
|                           <span slot="description"> |                         </div> | ||||||
|                             Blocks elevated system access from the add-on |  | ||||||
|                           </span> |  | ||||||
|                         <ha-switch |                         <ha-switch | ||||||
|                           @change=${this._protectionToggled} |                           @change=${this._protectionToggled} | ||||||
|                           .checked=${this.addon.protected} |                           .checked=${this.addon.protected} | ||||||
|                           haptic |                           haptic | ||||||
|                         ></ha-switch> |                         ></ha-switch> | ||||||
|                         </ha-settings-row> |                       </div> | ||||||
|                     ` |                     ` | ||||||
|                   : ""} |                   : ""} | ||||||
|                 </div> |  | ||||||
|               ` |               ` | ||||||
|             : ""} |             : ""} | ||||||
|           ${this._error ? html` <div class="errors">${this._error}</div> ` : ""} |           ${this._error ? html` <div class="errors">${this._error}</div> ` : ""} | ||||||
| @@ -503,9 +472,12 @@ class HassioAddonInfo extends LitElement { | |||||||
|                       </ha-call-api-button> |                       </ha-call-api-button> | ||||||
|                     ` |                     ` | ||||||
|                   : html` |                   : html` | ||||||
|                       <ha-progress-button @click=${this._startClicked}> |                       <ha-call-api-button | ||||||
|  |                         .hass=${this.hass} | ||||||
|  |                         .path="hassio/addons/${this.addon.slug}/start" | ||||||
|  |                       > | ||||||
|                         Start |                         Start | ||||||
|                       </ha-progress-button> |                       </ha-call-api-button> | ||||||
|                     `} |                     `} | ||||||
|                 ${this._computeShowWebUI |                 ${this._computeShowWebUI | ||||||
|                   ? html` |                   ? html` | ||||||
| @@ -529,12 +501,12 @@ class HassioAddonInfo extends LitElement { | |||||||
|                       </mwc-button> |                       </mwc-button> | ||||||
|                     ` |                     ` | ||||||
|                   : ""} |                   : ""} | ||||||
|                 <ha-progress-button |                 <mwc-button | ||||||
|                   class=" right warning" |                   class=" right warning" | ||||||
|                   @click=${this._uninstallClicked} |                   @click=${this._uninstallClicked} | ||||||
|                 > |                 > | ||||||
|                   Uninstall |                   Uninstall | ||||||
|                 </ha-progress-button> |                 </mwc-button> | ||||||
|                 ${this.addon.build |                 ${this.addon.build | ||||||
|                   ? html` |                   ? html` | ||||||
|                       <ha-call-api-button |                       <ha-call-api-button | ||||||
| @@ -556,7 +528,8 @@ class HassioAddonInfo extends LitElement { | |||||||
|                     ` |                     ` | ||||||
|                   : ""} |                   : ""} | ||||||
|                 <ha-progress-button |                 <ha-progress-button | ||||||
|                   .disabled=${!this.addon.available} |                   .disabled=${!this.addon.available || this._installing} | ||||||
|  |                   .progress=${this._installing} | ||||||
|                   @click=${this._installClicked} |                   @click=${this._installClicked} | ||||||
|                 > |                 > | ||||||
|                   Install |                   Install | ||||||
| @@ -579,6 +552,137 @@ class HassioAddonInfo extends LitElement { | |||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   static get styles(): CSSResult[] { | ||||||
|  |     return [ | ||||||
|  |       haStyle, | ||||||
|  |       hassioStyle, | ||||||
|  |       css` | ||||||
|  |         :host { | ||||||
|  |           display: block; | ||||||
|  |         } | ||||||
|  |         ha-card { | ||||||
|  |           display: block; | ||||||
|  |           margin-bottom: 16px; | ||||||
|  |         } | ||||||
|  |         ha-card.warning { | ||||||
|  |           background-color: var(--error-color); | ||||||
|  |           color: white; | ||||||
|  |         } | ||||||
|  |         ha-card.warning .card-header { | ||||||
|  |           color: white; | ||||||
|  |         } | ||||||
|  |         ha-card.warning .card-content { | ||||||
|  |           color: white; | ||||||
|  |         } | ||||||
|  |         ha-card.warning mwc-button { | ||||||
|  |           --mdc-theme-primary: white !important; | ||||||
|  |         } | ||||||
|  |         .warning { | ||||||
|  |           color: var(--error-color); | ||||||
|  |           --mdc-theme-primary: var(--error-color); | ||||||
|  |         } | ||||||
|  |         .light-color { | ||||||
|  |           color: var(--secondary-text-color); | ||||||
|  |         } | ||||||
|  |         .addon-header { | ||||||
|  |           padding-left: 8px; | ||||||
|  |           font-size: 24px; | ||||||
|  |           color: var(--ha-card-header-color, --primary-text-color); | ||||||
|  |         } | ||||||
|  |         .addon-version { | ||||||
|  |           float: right; | ||||||
|  |           font-size: 15px; | ||||||
|  |           vertical-align: middle; | ||||||
|  |         } | ||||||
|  |         .errors { | ||||||
|  |           color: var(--error-color); | ||||||
|  |           margin-bottom: 16px; | ||||||
|  |         } | ||||||
|  |         .description { | ||||||
|  |           margin-bottom: 16px; | ||||||
|  |         } | ||||||
|  |         img.logo { | ||||||
|  |           max-height: 60px; | ||||||
|  |           margin: 16px 0; | ||||||
|  |           display: block; | ||||||
|  |         } | ||||||
|  |         .state { | ||||||
|  |           display: flex; | ||||||
|  |           margin: 33px 0; | ||||||
|  |         } | ||||||
|  |         .state div { | ||||||
|  |           width: 180px; | ||||||
|  |           display: inline-block; | ||||||
|  |         } | ||||||
|  |         .state ha-svg-icon { | ||||||
|  |           width: 16px; | ||||||
|  |           height: 16px; | ||||||
|  |           color: var(--secondary-text-color); | ||||||
|  |         } | ||||||
|  |         ha-switch { | ||||||
|  |           display: flex; | ||||||
|  |         } | ||||||
|  |         ha-svg-icon.running { | ||||||
|  |           color: var(--paper-green-400); | ||||||
|  |         } | ||||||
|  |         ha-svg-icon.stopped { | ||||||
|  |           color: var(--google-red-300); | ||||||
|  |         } | ||||||
|  |         ha-call-api-button { | ||||||
|  |           font-weight: 500; | ||||||
|  |           color: var(--primary-color); | ||||||
|  |         } | ||||||
|  |         .right { | ||||||
|  |           float: right; | ||||||
|  |         } | ||||||
|  |         protection-enable mwc-button { | ||||||
|  |           --mdc-theme-primary: white; | ||||||
|  |         } | ||||||
|  |         .description a { | ||||||
|  |           color: var(--primary-color); | ||||||
|  |         } | ||||||
|  |         .red { | ||||||
|  |           --ha-label-badge-color: var(--label-badge-red, #df4c1e); | ||||||
|  |         } | ||||||
|  |         .blue { | ||||||
|  |           --ha-label-badge-color: var(--label-badge-blue, #039be5); | ||||||
|  |         } | ||||||
|  |         .green { | ||||||
|  |           --ha-label-badge-color: var(--label-badge-green, #0da035); | ||||||
|  |         } | ||||||
|  |         .yellow { | ||||||
|  |           --ha-label-badge-color: var(--label-badge-yellow, #f4b400); | ||||||
|  |         } | ||||||
|  |         .security { | ||||||
|  |           margin-bottom: 16px; | ||||||
|  |         } | ||||||
|  |         .card-actions { | ||||||
|  |           display: flow-root; | ||||||
|  |         } | ||||||
|  |         .security h3 { | ||||||
|  |           margin-bottom: 8px; | ||||||
|  |           font-weight: normal; | ||||||
|  |         } | ||||||
|  |         .security ha-label-badge { | ||||||
|  |           cursor: pointer; | ||||||
|  |           margin-right: 4px; | ||||||
|  |           --ha-label-badge-padding: 8px 0 0 0; | ||||||
|  |         } | ||||||
|  |         .changelog { | ||||||
|  |           display: contents; | ||||||
|  |         } | ||||||
|  |         .changelog-link { | ||||||
|  |           color: var(--primary-color); | ||||||
|  |           text-decoration: underline; | ||||||
|  |           cursor: pointer; | ||||||
|  |         } | ||||||
|  |         ha-markdown { | ||||||
|  |           padding: 16px; | ||||||
|  |         } | ||||||
|  |       `, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private get _computeHassioApi(): boolean { |   private get _computeHassioApi(): boolean { | ||||||
|     return ( |     return ( | ||||||
|       this.addon.hassio_api && |       this.addon.hassio_api && | ||||||
| @@ -663,29 +767,7 @@ class HassioAddonInfo extends LitElement { | |||||||
|       }; |       }; | ||||||
|       fireEvent(this, "hass-api-called", eventdata); |       fireEvent(this, "hass-api-called", eventdata); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       this._error = `Failed to set addon option, ${extractApiErrorMessage( |       this._error = `Failed to set addon option, ${err.body?.message || err}`; | ||||||
|         err |  | ||||||
|       )}`; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _watchdogToggled(): Promise<void> { |  | ||||||
|     this._error = undefined; |  | ||||||
|     const data: HassioAddonSetOptionParams = { |  | ||||||
|       watchdog: !this.addon.watchdog, |  | ||||||
|     }; |  | ||||||
|     try { |  | ||||||
|       await setHassioAddonOption(this.hass, this.addon.slug, data); |  | ||||||
|       const eventdata = { |  | ||||||
|         success: true, |  | ||||||
|         response: undefined, |  | ||||||
|         path: "option", |  | ||||||
|       }; |  | ||||||
|       fireEvent(this, "hass-api-called", eventdata); |  | ||||||
|     } catch (err) { |  | ||||||
|       this._error = `Failed to set addon option, ${extractApiErrorMessage( |  | ||||||
|         err |  | ||||||
|       )}`; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -703,9 +785,7 @@ class HassioAddonInfo extends LitElement { | |||||||
|       }; |       }; | ||||||
|       fireEvent(this, "hass-api-called", eventdata); |       fireEvent(this, "hass-api-called", eventdata); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       this._error = `Failed to set addon option, ${extractApiErrorMessage( |       this._error = `Failed to set addon option, ${err.body?.message || err}`; | ||||||
|         err |  | ||||||
|       )}`; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -723,9 +803,9 @@ class HassioAddonInfo extends LitElement { | |||||||
|       }; |       }; | ||||||
|       fireEvent(this, "hass-api-called", eventdata); |       fireEvent(this, "hass-api-called", eventdata); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       this._error = `Failed to set addon security option, ${extractApiErrorMessage( |       this._error = `Failed to set addon security option, ${ | ||||||
|         err |         err.body?.message || err | ||||||
|       )}`; |       }`; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -743,13 +823,12 @@ class HassioAddonInfo extends LitElement { | |||||||
|       }; |       }; | ||||||
|       fireEvent(this, "hass-api-called", eventdata); |       fireEvent(this, "hass-api-called", eventdata); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       this._error = `Failed to set addon option, ${extractApiErrorMessage( |       this._error = `Failed to set addon option, ${err.body?.message || err}`; | ||||||
|         err |  | ||||||
|       )}`; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _openChangelog(): Promise<void> { |   private async _openChangelog(): Promise<void> { | ||||||
|  |     this._error = undefined; | ||||||
|     try { |     try { | ||||||
|       const content = await fetchHassioAddonChangelog( |       const content = await fetchHassioAddonChangelog( | ||||||
|         this.hass, |         this.hass, | ||||||
| @@ -760,17 +839,15 @@ class HassioAddonInfo extends LitElement { | |||||||
|         content, |         content, | ||||||
|       }); |       }); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       showAlertDialog(this, { |       this._error = `Failed to get addon changelog, ${ | ||||||
|         title: "Failed to get addon changelog", |         err.body?.message || err | ||||||
|         text: extractApiErrorMessage(err), |       }`; | ||||||
|       }); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _installClicked(ev: CustomEvent): Promise<void> { |   private async _installClicked(): Promise<void> { | ||||||
|     const button = ev.currentTarget as any; |     this._error = undefined; | ||||||
|     button.progress = true; |     this._installing = true; | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       await installHassioAddon(this.hass, this.addon.slug); |       await installHassioAddon(this.hass, this.addon.slug); | ||||||
|       const eventdata = { |       const eventdata = { | ||||||
| @@ -780,62 +857,12 @@ class HassioAddonInfo extends LitElement { | |||||||
|       }; |       }; | ||||||
|       fireEvent(this, "hass-api-called", eventdata); |       fireEvent(this, "hass-api-called", eventdata); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       showAlertDialog(this, { |       this._error = `Failed to install addon, ${err.body?.message || err}`; | ||||||
|         title: "Failed to install addon", |  | ||||||
|         text: extractApiErrorMessage(err), |  | ||||||
|       }); |  | ||||||
|     } |     } | ||||||
|     button.progress = false; |     this._installing = false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _startClicked(ev: CustomEvent): Promise<void> { |   private async _uninstallClicked(): Promise<void> { | ||||||
|     const button = ev.currentTarget as any; |  | ||||||
|     button.progress = true; |  | ||||||
|     try { |  | ||||||
|       const validate = await validateHassioAddonOption( |  | ||||||
|         this.hass, |  | ||||||
|         this.addon.slug |  | ||||||
|       ); |  | ||||||
|       if (!validate.data.valid) { |  | ||||||
|         await showConfirmationDialog(this, { |  | ||||||
|           title: "Failed to start addon - configruation validation faled!", |  | ||||||
|           text: validate.data.message.split(" Got ")[0], |  | ||||||
|           confirm: () => this._openConfiguration(), |  | ||||||
|           confirmText: "Go to configruation", |  | ||||||
|           dismissText: "Cancel", |  | ||||||
|         }); |  | ||||||
|         button.progress = false; |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|     } catch (err) { |  | ||||||
|       showAlertDialog(this, { |  | ||||||
|         title: "Failed to validate addon configuration", |  | ||||||
|         text: extractApiErrorMessage(err), |  | ||||||
|       }); |  | ||||||
|       button.progress = false; |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       await startHassioAddon(this.hass, this.addon.slug); |  | ||||||
|       this.addon = await fetchHassioAddonInfo(this.hass, this.addon.slug); |  | ||||||
|     } catch (err) { |  | ||||||
|       showAlertDialog(this, { |  | ||||||
|         title: "Failed to start addon", |  | ||||||
|         text: extractApiErrorMessage(err), |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|     button.progress = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _openConfiguration(): void { |  | ||||||
|     navigate(this, `/hassio/addon/${this.addon.slug}/config`); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _uninstallClicked(ev: CustomEvent): Promise<void> { |  | ||||||
|     const button = ev.currentTarget as any; |  | ||||||
|     button.progress = true; |  | ||||||
|  |  | ||||||
|     const confirmed = await showConfirmationDialog(this, { |     const confirmed = await showConfirmationDialog(this, { | ||||||
|       title: this.addon.name, |       title: this.addon.name, | ||||||
|       text: "Are you sure you want to uninstall this add-on?", |       text: "Are you sure you want to uninstall this add-on?", | ||||||
| @@ -844,7 +871,6 @@ class HassioAddonInfo extends LitElement { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (!confirmed) { |     if (!confirmed) { | ||||||
|       button.progress = false; |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -858,152 +884,8 @@ class HassioAddonInfo extends LitElement { | |||||||
|       }; |       }; | ||||||
|       fireEvent(this, "hass-api-called", eventdata); |       fireEvent(this, "hass-api-called", eventdata); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       showAlertDialog(this, { |       this._error = `Failed to uninstall addon, ${err.body?.message || err}`; | ||||||
|         title: "Failed to uninstall addon", |  | ||||||
|         text: extractApiErrorMessage(err), |  | ||||||
|       }); |  | ||||||
|     } |     } | ||||||
|     button.progress = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult[] { |  | ||||||
|     return [ |  | ||||||
|       haStyle, |  | ||||||
|       hassioStyle, |  | ||||||
|       css` |  | ||||||
|         :host { |  | ||||||
|           display: block; |  | ||||||
|         } |  | ||||||
|         ha-card { |  | ||||||
|           display: block; |  | ||||||
|           margin-bottom: 16px; |  | ||||||
|         } |  | ||||||
|         ha-card.warning { |  | ||||||
|           background-color: var(--error-color); |  | ||||||
|           color: white; |  | ||||||
|         } |  | ||||||
|         ha-card.warning .card-header { |  | ||||||
|           color: white; |  | ||||||
|         } |  | ||||||
|         ha-card.warning .card-content { |  | ||||||
|           color: white; |  | ||||||
|         } |  | ||||||
|         ha-card.warning mwc-button { |  | ||||||
|           --mdc-theme-primary: white !important; |  | ||||||
|         } |  | ||||||
|         .warning { |  | ||||||
|           color: var(--error-color); |  | ||||||
|           --mdc-theme-primary: var(--error-color); |  | ||||||
|         } |  | ||||||
|         .light-color { |  | ||||||
|           color: var(--secondary-text-color); |  | ||||||
|         } |  | ||||||
|         .addon-header { |  | ||||||
|           padding-left: 8px; |  | ||||||
|           font-size: 24px; |  | ||||||
|           color: var(--ha-card-header-color, --primary-text-color); |  | ||||||
|         } |  | ||||||
|         .addon-version { |  | ||||||
|           float: right; |  | ||||||
|           font-size: 15px; |  | ||||||
|           vertical-align: middle; |  | ||||||
|         } |  | ||||||
|         .errors { |  | ||||||
|           color: var(--error-color); |  | ||||||
|           margin-bottom: 16px; |  | ||||||
|         } |  | ||||||
|         .description { |  | ||||||
|           margin-bottom: 16px; |  | ||||||
|         } |  | ||||||
|         img.logo { |  | ||||||
|           max-height: 60px; |  | ||||||
|           margin: 16px 0; |  | ||||||
|           display: block; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         ha-switch { |  | ||||||
|           display: flex; |  | ||||||
|         } |  | ||||||
|         ha-svg-icon.running { |  | ||||||
|           color: var(--paper-green-400); |  | ||||||
|         } |  | ||||||
|         ha-svg-icon.stopped { |  | ||||||
|           color: var(--google-red-300); |  | ||||||
|         } |  | ||||||
|         ha-call-api-button { |  | ||||||
|           font-weight: 500; |  | ||||||
|           color: var(--primary-color); |  | ||||||
|         } |  | ||||||
|         .right { |  | ||||||
|           float: right; |  | ||||||
|         } |  | ||||||
|         protection-enable mwc-button { |  | ||||||
|           --mdc-theme-primary: white; |  | ||||||
|         } |  | ||||||
|         .description a { |  | ||||||
|           color: var(--primary-color); |  | ||||||
|         } |  | ||||||
|         .red { |  | ||||||
|           --ha-label-badge-color: var(--label-badge-red, #df4c1e); |  | ||||||
|         } |  | ||||||
|         .blue { |  | ||||||
|           --ha-label-badge-color: var(--label-badge-blue, #039be5); |  | ||||||
|         } |  | ||||||
|         .green { |  | ||||||
|           --ha-label-badge-color: var(--label-badge-green, #0da035); |  | ||||||
|         } |  | ||||||
|         .yellow { |  | ||||||
|           --ha-label-badge-color: var(--label-badge-yellow, #f4b400); |  | ||||||
|         } |  | ||||||
|         .security { |  | ||||||
|           margin-bottom: 16px; |  | ||||||
|         } |  | ||||||
|         .card-actions { |  | ||||||
|           display: flow-root; |  | ||||||
|         } |  | ||||||
|         .security h3 { |  | ||||||
|           margin-bottom: 8px; |  | ||||||
|           font-weight: normal; |  | ||||||
|         } |  | ||||||
|         .security ha-label-badge { |  | ||||||
|           cursor: pointer; |  | ||||||
|           margin-right: 4px; |  | ||||||
|           --ha-label-badge-padding: 8px 0 0 0; |  | ||||||
|         } |  | ||||||
|         .changelog { |  | ||||||
|           display: contents; |  | ||||||
|         } |  | ||||||
|         .changelog-link { |  | ||||||
|           color: var(--primary-color); |  | ||||||
|           text-decoration: underline; |  | ||||||
|           cursor: pointer; |  | ||||||
|         } |  | ||||||
|         ha-markdown { |  | ||||||
|           padding: 16px; |  | ||||||
|         } |  | ||||||
|         ha-settings-row { |  | ||||||
|           padding: 0; |  | ||||||
|           height: 54px; |  | ||||||
|           width: 100%; |  | ||||||
|         } |  | ||||||
|         ha-settings-row > span[slot="description"] { |  | ||||||
|           white-space: normal; |  | ||||||
|           color: var(--secondary-text-color); |  | ||||||
|         } |  | ||||||
|         ha-settings-row[three-line] { |  | ||||||
|           height: 74px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .addon-options { |  | ||||||
|           max-width: 50%; |  | ||||||
|         } |  | ||||||
|         @media (max-width: 720px) { |  | ||||||
|           .addon-options { |  | ||||||
|             max-width: 100%; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       `, |  | ||||||
|     ]; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -4,9 +4,9 @@ import { | |||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
| @@ -14,7 +14,6 @@ import { | |||||||
|   fetchHassioAddonLogs, |   fetchHassioAddonLogs, | ||||||
|   HassioAddonDetails, |   HassioAddonDetails, | ||||||
| } from "../../../../src/data/hassio/addon"; | } from "../../../../src/data/hassio/addon"; | ||||||
| import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; |  | ||||||
| import { haStyle } from "../../../../src/resources/styles"; | import { haStyle } from "../../../../src/resources/styles"; | ||||||
| import { HomeAssistant } from "../../../../src/types"; | import { HomeAssistant } from "../../../../src/types"; | ||||||
| import "../../components/hassio-ansi-to-html"; | import "../../components/hassio-ansi-to-html"; | ||||||
| @@ -76,7 +75,7 @@ class HassioAddonLogs extends LitElement { | |||||||
|     try { |     try { | ||||||
|       this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug); |       this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       this._error = `Failed to get addon logs, ${extractApiErrorMessage(err)}`; |       this._error = `Failed to get addon logs, ${err.body?.message || err}`; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ interface State { | |||||||
| class HassioAnsiToHtml extends LitElement { | class HassioAnsiToHtml extends LitElement { | ||||||
|   @property() public content!: string; |   @property() public content!: string; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult | void { |   public render(): TemplateResult | void { | ||||||
|     return html`${this._parseTextToColoredPre(this.content)}`; |     return html`${this._parseTextToColoredPre(this.content)}`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,27 +5,19 @@ import { | |||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import "../../../src/components/buttons/ha-progress-button"; | import "../../../src/components/buttons/ha-call-api-button"; | ||||||
| import "../../../src/components/ha-card"; | import "../../../src/components/ha-card"; | ||||||
| import "../../../src/components/ha-svg-icon"; | import "../../../src/components/ha-svg-icon"; | ||||||
| import { |  | ||||||
|   extractApiErrorMessage, |  | ||||||
|   HassioResponse, |  | ||||||
| } from "../../../src/data/hassio/common"; |  | ||||||
| import { HassioHassOSInfo } from "../../../src/data/hassio/host"; | import { HassioHassOSInfo } from "../../../src/data/hassio/host"; | ||||||
| import { | import { | ||||||
|   HassioHomeAssistantInfo, |   HassioHomeAssistantInfo, | ||||||
|   HassioSupervisorInfo, |   HassioSupervisorInfo, | ||||||
| } from "../../../src/data/hassio/supervisor"; | } from "../../../src/data/hassio/supervisor"; | ||||||
| import { |  | ||||||
|   showAlertDialog, |  | ||||||
|   showConfirmationDialog, |  | ||||||
| } from "../../../src/dialogs/generic/show-dialog-box"; |  | ||||||
| import { haStyle } from "../../../src/resources/styles"; | import { haStyle } from "../../../src/resources/styles"; | ||||||
| import { HomeAssistant } from "../../../src/types"; | import { HomeAssistant } from "../../../src/types"; | ||||||
| import { hassioStyle } from "../resources/hassio-style"; | import { hassioStyle } from "../resources/hassio-style"; | ||||||
| @@ -134,47 +126,32 @@ export class HassioUpdate extends LitElement { | |||||||
|           <a href="${releaseNotesUrl}" target="_blank" rel="noreferrer"> |           <a href="${releaseNotesUrl}" target="_blank" rel="noreferrer"> | ||||||
|             <mwc-button>Release notes</mwc-button> |             <mwc-button>Release notes</mwc-button> | ||||||
|           </a> |           </a> | ||||||
|           <ha-progress-button |           <ha-call-api-button | ||||||
|             .apiPath=${apiPath} |             .hass=${this.hass} | ||||||
|             .name=${name} |             .path=${apiPath} | ||||||
|             .version=${lastVersion} |             @hass-api-called=${this._apiCalled} | ||||||
|             @click=${this._confirmUpdate} |  | ||||||
|           > |           > | ||||||
|             Update |             Update | ||||||
|           </ha-progress-button> |           </ha-call-api-button> | ||||||
|         </div> |         </div> | ||||||
|       </ha-card> |       </ha-card> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _confirmUpdate(ev): Promise<void> { |   private _apiCalled(ev): void { | ||||||
|     const item = ev.currentTarget; |     if (ev.detail.success) { | ||||||
|     item.progress = true; |       this._error = ""; | ||||||
|     const confirmed = await showConfirmationDialog(this, { |  | ||||||
|       title: `Update ${item.name}`, |  | ||||||
|       text: `Are you sure you want to upgrade ${item.name} to version ${item.version}?`, |  | ||||||
|       confirmText: "update", |  | ||||||
|       dismissText: "cancel", |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     if (!confirmed) { |  | ||||||
|       item.progress = false; |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     try { |  | ||||||
|       await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath); |     const response = ev.detail.response; | ||||||
|     } catch (err) { |  | ||||||
|       // Only show an error if the status code was not expected (user behind proxy) |     if (typeof response.body === "object") { | ||||||
|       // or no status at all(connection terminated) |       this._error = response.body.message || "Unknown error"; | ||||||
|       if (err.status_code && ![502, 503, 504].includes(err.status_code)) { |     } else { | ||||||
|         showAlertDialog(this, { |       this._error = response.body; | ||||||
|           title: "Update failed", |  | ||||||
|           text: extractApiErrorMessage(err), |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|     item.progress = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult[] { |   static get styles(): CSSResult[] { | ||||||
|     return [ |     return [ | ||||||
|   | |||||||
| @@ -1,333 +0,0 @@ | |||||||
| import "@material/mwc-button/mwc-button"; |  | ||||||
| import "@material/mwc-icon-button"; |  | ||||||
| import "@material/mwc-tab"; |  | ||||||
| import "@material/mwc-tab-bar"; |  | ||||||
| import { mdiClose } from "@mdi/js"; |  | ||||||
| import { PaperInputElement } from "@polymer/paper-input/paper-input"; |  | ||||||
| import { |  | ||||||
|   css, |  | ||||||
|   CSSResult, |  | ||||||
|   customElement, |  | ||||||
|   html, |  | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |  | ||||||
|   property, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit-element"; |  | ||||||
| import { cache } from "lit-html/directives/cache"; |  | ||||||
| import { fireEvent } from "../../../../src/common/dom/fire_event"; |  | ||||||
| import "../../../../src/components/ha-circular-progress"; |  | ||||||
| import "../../../../src/components/ha-dialog"; |  | ||||||
| import "../../../../src/components/ha-formfield"; |  | ||||||
| import "../../../../src/components/ha-header-bar"; |  | ||||||
| import "../../../../src/components/ha-radio"; |  | ||||||
| import type { HaRadio } from "../../../../src/components/ha-radio"; |  | ||||||
| import "../../../../src/components/ha-related-items"; |  | ||||||
| import "../../../../src/components/ha-svg-icon"; |  | ||||||
| import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; |  | ||||||
| import { |  | ||||||
|   NetworkInterface, |  | ||||||
|   updateNetworkInterface, |  | ||||||
| } from "../../../../src/data/hassio/network"; |  | ||||||
| import { |  | ||||||
|   showAlertDialog, |  | ||||||
|   showConfirmationDialog, |  | ||||||
| } from "../../../../src/dialogs/generic/show-dialog-box"; |  | ||||||
| import { HassDialog } from "../../../../src/dialogs/make-dialog-manager"; |  | ||||||
| import { haStyleDialog } from "../../../../src/resources/styles"; |  | ||||||
| import type { HomeAssistant } from "../../../../src/types"; |  | ||||||
| import { HassioNetworkDialogParams } from "./show-dialog-network"; |  | ||||||
|  |  | ||||||
| @customElement("dialog-hassio-network") |  | ||||||
| export class DialogHassioNetwork extends LitElement implements HassDialog { |  | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _prosessing = false; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _params?: HassioNetworkDialogParams; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _network!: { |  | ||||||
|     interface: string; |  | ||||||
|     data: NetworkInterface; |  | ||||||
|   }[]; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _curTabIndex = 0; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _device?: { |  | ||||||
|     interface: string; |  | ||||||
|     data: NetworkInterface; |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _dirty = false; |  | ||||||
|  |  | ||||||
|   public async showDialog(params: HassioNetworkDialogParams): Promise<void> { |  | ||||||
|     this._params = params; |  | ||||||
|     this._dirty = false; |  | ||||||
|     this._curTabIndex = 0; |  | ||||||
|     this._network = Object.keys(params.network?.interfaces) |  | ||||||
|       .map((device) => ({ |  | ||||||
|         interface: device, |  | ||||||
|         data: params.network.interfaces[device], |  | ||||||
|       })) |  | ||||||
|       .sort((a, b) => { |  | ||||||
|         return a.data.primary > b.data.primary ? -1 : 1; |  | ||||||
|       }); |  | ||||||
|     this._device = this._network[this._curTabIndex]; |  | ||||||
|     this._device.data.nameservers = String(this._device.data.nameservers); |  | ||||||
|     await this.updateComplete; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public closeDialog(): void { |  | ||||||
|     this._params = undefined; |  | ||||||
|     this._prosessing = false; |  | ||||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |  | ||||||
|     if (!this._params || !this._network) { |  | ||||||
|       return html``; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return html` |  | ||||||
|       <ha-dialog |  | ||||||
|         open |  | ||||||
|         scrimClickAction |  | ||||||
|         escapeKeyAction |  | ||||||
|         .heading=${true} |  | ||||||
|         hideActions |  | ||||||
|         @closed=${this.closeDialog} |  | ||||||
|       > |  | ||||||
|         <div slot="heading"> |  | ||||||
|           <ha-header-bar> |  | ||||||
|             <span slot="title"> |  | ||||||
|               Network settings |  | ||||||
|             </span> |  | ||||||
|             <mwc-icon-button slot="actionItems" dialogAction="cancel"> |  | ||||||
|               <ha-svg-icon .path=${mdiClose}></ha-svg-icon> |  | ||||||
|             </mwc-icon-button> |  | ||||||
|           </ha-header-bar> |  | ||||||
|           ${this._network.length > 1 |  | ||||||
|             ? html` <mwc-tab-bar |  | ||||||
|                 .activeIndex=${this._curTabIndex} |  | ||||||
|                 @MDCTabBar:activated=${this._handleTabActivated} |  | ||||||
|                 >${this._network.map( |  | ||||||
|                   (device) => |  | ||||||
|                     html`<mwc-tab |  | ||||||
|                       .id=${device.interface} |  | ||||||
|                       .label=${device.interface} |  | ||||||
|                     > |  | ||||||
|                     </mwc-tab>` |  | ||||||
|                 )} |  | ||||||
|               </mwc-tab-bar>` |  | ||||||
|             : ""} |  | ||||||
|         </div> |  | ||||||
|         ${cache(this._renderTab())} |  | ||||||
|       </ha-dialog> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _renderTab() { |  | ||||||
|     return html` <div class="form container"> |  | ||||||
|         <ha-formfield label="DHCP"> |  | ||||||
|           <ha-radio |  | ||||||
|             @change=${this._handleRadioValueChanged} |  | ||||||
|             value="dhcp" |  | ||||||
|             name="method" |  | ||||||
|             ?checked=${this._device!.data.method === "dhcp"} |  | ||||||
|           > |  | ||||||
|           </ha-radio> |  | ||||||
|         </ha-formfield> |  | ||||||
|         <ha-formfield label="Static"> |  | ||||||
|           <ha-radio |  | ||||||
|             @change=${this._handleRadioValueChanged} |  | ||||||
|             value="static" |  | ||||||
|             name="method" |  | ||||||
|             ?checked=${this._device!.data.method === "static"} |  | ||||||
|           > |  | ||||||
|           </ha-radio> |  | ||||||
|         </ha-formfield> |  | ||||||
|         ${this._device!.data.method !== "dhcp" |  | ||||||
|           ? html` <paper-input |  | ||||||
|                 class="flex-auto" |  | ||||||
|                 id="ip_address" |  | ||||||
|                 label="IP address/Netmask" |  | ||||||
|                 .value="${this._device!.data.ip_address}" |  | ||||||
|                 @value-changed=${this._handleInputValueChanged} |  | ||||||
|               ></paper-input> |  | ||||||
|               <paper-input |  | ||||||
|                 class="flex-auto" |  | ||||||
|                 id="gateway" |  | ||||||
|                 label="Gateway address" |  | ||||||
|                 .value="${this._device!.data.gateway}" |  | ||||||
|                 @value-changed=${this._handleInputValueChanged} |  | ||||||
|               ></paper-input> |  | ||||||
|               <paper-input |  | ||||||
|                 class="flex-auto" |  | ||||||
|                 id="nameservers" |  | ||||||
|                 label="DNS servers" |  | ||||||
|                 .value="${this._device!.data.nameservers as string}" |  | ||||||
|                 @value-changed=${this._handleInputValueChanged} |  | ||||||
|               ></paper-input> |  | ||||||
|               NB!: If you are changing IP or gateway addresses, you might lose |  | ||||||
|               the connection.` |  | ||||||
|           : ""} |  | ||||||
|       </div> |  | ||||||
|       <div class="buttons"> |  | ||||||
|         <mwc-button label="close" @click=${this.closeDialog}> </mwc-button> |  | ||||||
|         <mwc-button @click=${this._updateNetwork} ?disabled=${!this._dirty}> |  | ||||||
|           ${this._prosessing |  | ||||||
|             ? html`<ha-circular-progress active></ha-circular-progress>` |  | ||||||
|             : "Update"} |  | ||||||
|         </mwc-button> |  | ||||||
|       </div>`; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _updateNetwork() { |  | ||||||
|     this._prosessing = true; |  | ||||||
|     let options: Partial<NetworkInterface> = { |  | ||||||
|       method: this._device!.data.method, |  | ||||||
|     }; |  | ||||||
|     if (options.method !== "dhcp") { |  | ||||||
|       options = { |  | ||||||
|         ...options, |  | ||||||
|         address: this._device!.data.ip_address, |  | ||||||
|         gateway: this._device!.data.gateway, |  | ||||||
|         dns: String(this._device!.data.nameservers).split(","), |  | ||||||
|       }; |  | ||||||
|     } |  | ||||||
|     try { |  | ||||||
|       await updateNetworkInterface(this.hass, this._device!.interface, options); |  | ||||||
|     } catch (err) { |  | ||||||
|       showAlertDialog(this, { |  | ||||||
|         title: "Failed to change network settings", |  | ||||||
|         text: extractApiErrorMessage(err), |  | ||||||
|       }); |  | ||||||
|       this._prosessing = false; |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     this._params?.loadData(); |  | ||||||
|     this.closeDialog(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _handleTabActivated(ev: CustomEvent): Promise<void> { |  | ||||||
|     if (this._dirty) { |  | ||||||
|       const confirm = await showConfirmationDialog(this, { |  | ||||||
|         text: |  | ||||||
|           "You have unsaved changes, these will get lost if you change tabs, do you want to continue?", |  | ||||||
|         confirmText: "yes", |  | ||||||
|         dismissText: "no", |  | ||||||
|       }); |  | ||||||
|       if (!confirm) { |  | ||||||
|         this.requestUpdate("_device"); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     this._curTabIndex = ev.detail.index; |  | ||||||
|     this._device = this._network[ev.detail.index]; |  | ||||||
|     this._device.data.nameservers = String(this._device.data.nameservers); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _handleRadioValueChanged(ev: CustomEvent): void { |  | ||||||
|     const value = (ev.target as HaRadio).value as "dhcp" | "static"; |  | ||||||
|  |  | ||||||
|     if (!value || !this._device || this._device!.data.method === value) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this._dirty = true; |  | ||||||
|  |  | ||||||
|     this._device!.data.method = value; |  | ||||||
|     this.requestUpdate("_device"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _handleInputValueChanged(ev: CustomEvent): void { |  | ||||||
|     const value: string | null | undefined = (ev.target as PaperInputElement) |  | ||||||
|       .value; |  | ||||||
|     const id = (ev.target as PaperInputElement).id; |  | ||||||
|  |  | ||||||
|     if (!value || !this._device || this._device.data[id] === value) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this._dirty = true; |  | ||||||
|  |  | ||||||
|     this._device.data[id] = value; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult[] { |  | ||||||
|     return [ |  | ||||||
|       haStyleDialog, |  | ||||||
|       css` |  | ||||||
|         ha-header-bar { |  | ||||||
|           --mdc-theme-on-primary: var(--primary-text-color); |  | ||||||
|           --mdc-theme-primary: var(--mdc-theme-surface); |  | ||||||
|           flex-shrink: 0; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         mwc-tab-bar { |  | ||||||
|           border-bottom: 1px solid |  | ||||||
|             var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         ha-dialog { |  | ||||||
|           --dialog-content-position: static; |  | ||||||
|           --dialog-content-padding: 0; |  | ||||||
|           --dialog-z-index: 6; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @media all and (min-width: 451px) and (min-height: 501px) { |  | ||||||
|           .container { |  | ||||||
|             width: 400px; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .content { |  | ||||||
|           display: block; |  | ||||||
|           padding: 20px 24px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /* overrule the ha-style-dialog max-height on small screens */ |  | ||||||
|         @media all and (max-width: 450px), all and (max-height: 500px) { |  | ||||||
|           ha-header-bar { |  | ||||||
|             --mdc-theme-primary: var(--app-header-background-color); |  | ||||||
|             --mdc-theme-on-primary: var(--app-header-text-color, white); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         mwc-button.warning { |  | ||||||
|           --mdc-theme-primary: var(--error-color); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([rtl]) app-toolbar { |  | ||||||
|           direction: rtl; |  | ||||||
|           text-align: right; |  | ||||||
|         } |  | ||||||
|         .container { |  | ||||||
|           padding: 20px 24px; |  | ||||||
|         } |  | ||||||
|         .form { |  | ||||||
|           margin-bottom: 53px; |  | ||||||
|         } |  | ||||||
|         .buttons { |  | ||||||
|           position: absolute; |  | ||||||
|           bottom: 0; |  | ||||||
|           width: 100%; |  | ||||||
|           box-sizing: border-box; |  | ||||||
|           border-top: 1px solid |  | ||||||
|             var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); |  | ||||||
|           display: flex; |  | ||||||
|           justify-content: space-between; |  | ||||||
|           padding: 8px; |  | ||||||
|           padding-bottom: max(env(safe-area-inset-bottom), 8px); |  | ||||||
|           background-color: var(--mdc-theme-surface, #fff); |  | ||||||
|         } |  | ||||||
|       `, |  | ||||||
|     ]; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "dialog-hassio-network": DialogHassioNetwork; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,22 +0,0 @@ | |||||||
| import { fireEvent } from "../../../../src/common/dom/fire_event"; |  | ||||||
| import { NetworkInfo } from "../../../../src/data/hassio/network"; |  | ||||||
| import "./dialog-hassio-network"; |  | ||||||
|  |  | ||||||
| export interface HassioNetworkDialogParams { |  | ||||||
|   network: NetworkInfo; |  | ||||||
|   loadData: () => Promise<void>; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const showNetworkDialog = ( |  | ||||||
|   element: HTMLElement, |  | ||||||
|   dialogParams: HassioNetworkDialogParams |  | ||||||
| ): void => { |  | ||||||
|   fireEvent(element, "show-dialog", { |  | ||||||
|     dialogTag: "dialog-hassio-network", |  | ||||||
|     dialogImport: () => |  | ||||||
|       import( |  | ||||||
|         /* webpackChunkName: "dialog-hassio-network" */ "./dialog-hassio-network" |  | ||||||
|       ), |  | ||||||
|     dialogParams, |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| @@ -5,26 +5,25 @@ import "@polymer/paper-input/paper-input"; | |||||||
| import type { PaperInputElement } from "@polymer/paper-input/paper-input"; | import type { PaperInputElement } from "@polymer/paper-input/paper-input"; | ||||||
| import "@polymer/paper-item/paper-item"; | import "@polymer/paper-item/paper-item"; | ||||||
| import "@polymer/paper-item/paper-item-body"; | import "@polymer/paper-item/paper-item-body"; | ||||||
|  | import "../../../../src/components/ha-circular-progress"; | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   query, |   query, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import "../../../../src/components/ha-circular-progress"; |  | ||||||
| import "../../../../src/components/ha-dialog"; | import "../../../../src/components/ha-dialog"; | ||||||
| import "../../../../src/components/ha-svg-icon"; | import "../../../../src/components/ha-svg-icon"; | ||||||
| import { | import { | ||||||
|   fetchHassioAddonsInfo, |   fetchHassioAddonsInfo, | ||||||
|   HassioAddonRepository, |   HassioAddonRepository, | ||||||
| } from "../../../../src/data/hassio/addon"; | } from "../../../../src/data/hassio/addon"; | ||||||
| import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; |  | ||||||
| import { setSupervisorOption } from "../../../../src/data/hassio/supervisor"; | import { setSupervisorOption } from "../../../../src/data/hassio/supervisor"; | ||||||
| import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; | import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; | ||||||
| import type { HomeAssistant } from "../../../../src/types"; | import type { HomeAssistant } from "../../../../src/types"; | ||||||
| @@ -191,7 +190,7 @@ class HassioRepositoriesDialog extends LitElement { | |||||||
|  |  | ||||||
|       input.value = ""; |       input.value = ""; | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       this._error = extractApiErrorMessage(err); |       this._error = err.message; | ||||||
|     } |     } | ||||||
|     this._prosessing = false; |     this._prosessing = false; | ||||||
|   } |   } | ||||||
| @@ -223,7 +222,7 @@ class HassioRepositoriesDialog extends LitElement { | |||||||
|  |  | ||||||
|       await this._dialogParams!.loadData(); |       await this._dialogParams!.loadData(); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       this._error = extractApiErrorMessage(err); |       this._error = err.message; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,20 +7,18 @@ import { | |||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { createCloseHeading } from "../../../../src/components/ha-dialog"; | import { createCloseHeading } from "../../../../src/components/ha-dialog"; | ||||||
| import "../../../../src/components/ha-svg-icon"; | import "../../../../src/components/ha-svg-icon"; | ||||||
| import { getSignedPath } from "../../../../src/data/auth"; | import { getSignedPath } from "../../../../src/data/auth"; | ||||||
| import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; |  | ||||||
| import { | import { | ||||||
|   fetchHassioSnapshotInfo, |   fetchHassioSnapshotInfo, | ||||||
|   HassioSnapshotDetail, |   HassioSnapshotDetail, | ||||||
| } from "../../../../src/data/hassio/snapshot"; | } from "../../../../src/data/hassio/snapshot"; | ||||||
| import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box"; |  | ||||||
| import { PolymerChangedEvent } from "../../../../src/polymer-types"; | import { PolymerChangedEvent } from "../../../../src/polymer-types"; | ||||||
| import { haStyleDialog } from "../../../../src/resources/styles"; | import { haStyleDialog } from "../../../../src/resources/styles"; | ||||||
| import { HomeAssistant } from "../../../../src/types"; | import { HomeAssistant } from "../../../../src/types"; | ||||||
| @@ -268,12 +266,8 @@ class HassioSnapshotDialog extends LitElement { | |||||||
|     this._snapshotPassword = ev.detail.value; |     this._snapshotPassword = ev.detail.value; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _partialRestoreClicked() { |   private _partialRestoreClicked() { | ||||||
|     if ( |     if (!confirm("Are you sure you want to restore this snapshot?")) { | ||||||
|       !(await showConfirmationDialog(this, { |  | ||||||
|         title: "Are you sure you want partially to restore this snapshot?", |  | ||||||
|       })) |  | ||||||
|     ) { |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -318,13 +312,8 @@ class HassioSnapshotDialog extends LitElement { | |||||||
|       ); |       ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _fullRestoreClicked() { |   private _fullRestoreClicked() { | ||||||
|     if ( |     if (!confirm("Are you sure you want to restore this snapshot?")) { | ||||||
|       !(await showConfirmationDialog(this, { |  | ||||||
|         title: |  | ||||||
|           "Are you sure you want to wipe your system and restore this snapshot?", |  | ||||||
|       })) |  | ||||||
|     ) { |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -349,12 +338,8 @@ class HassioSnapshotDialog extends LitElement { | |||||||
|       ); |       ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _deleteClicked() { |   private _deleteClicked() { | ||||||
|     if ( |     if (!confirm("Are you sure you want to delete this snapshot?")) { | ||||||
|       !(await showConfirmationDialog(this, { |  | ||||||
|         title: "Are you sure you want to delete this snapshot?", |  | ||||||
|       })) |  | ||||||
|     ) { |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -380,7 +365,7 @@ class HassioSnapshotDialog extends LitElement { | |||||||
|         `/api/hassio/snapshots/${this._snapshot!.slug}/download` |         `/api/hassio/snapshots/${this._snapshot!.slug}/download` | ||||||
|       ); |       ); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       alert(`Error: ${extractApiErrorMessage(err)}`); |       alert(`Error: ${err.message}`); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ import { | |||||||
|   HassioAddonDetails, |   HassioAddonDetails, | ||||||
|   restartHassioAddon, |   restartHassioAddon, | ||||||
| } from "../../../src/data/hassio/addon"; | } from "../../../src/data/hassio/addon"; | ||||||
| import { extractApiErrorMessage } from "../../../src/data/hassio/common"; |  | ||||||
| import { | import { | ||||||
|   showAlertDialog, |   showAlertDialog, | ||||||
|   showConfirmationDialog, |   showConfirmationDialog, | ||||||
| @@ -27,7 +26,7 @@ export const suggestAddonRestart = async ( | |||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       showAlertDialog(element, { |       showAlertDialog(element, { | ||||||
|         title: "Failed to restart", |         title: "Failed to restart", | ||||||
|         text: extractApiErrorMessage(err), |         text: err.body.message, | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -106,9 +106,7 @@ export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) { | |||||||
|         }; |         }; | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|       themeName = |       themeName = (this.hass.selectedTheme as unknown) as string; | ||||||
|         ((this.hass.selectedTheme as unknown) as string) || |  | ||||||
|         this.hass.themes.default_theme; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     applyThemesOnElement( |     applyThemesOnElement( | ||||||
|   | |||||||
| @@ -13,17 +13,15 @@ import { | |||||||
|   CSSResultArray, |   CSSResultArray, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   PropertyValues, |   PropertyValues, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { fireEvent } from "../../../src/common/dom/fire_event"; | import { fireEvent } from "../../../src/common/dom/fire_event"; | ||||||
| import "../../../src/components/buttons/ha-progress-button"; |  | ||||||
| import "../../../src/components/ha-card"; | import "../../../src/components/ha-card"; | ||||||
| import "../../../src/components/ha-svg-icon"; | import "../../../src/components/ha-svg-icon"; | ||||||
| import { extractApiErrorMessage } from "../../../src/data/hassio/common"; |  | ||||||
| import { | import { | ||||||
|   createHassioFullSnapshot, |   createHassioFullSnapshot, | ||||||
|   createHassioPartialSnapshot, |   createHassioPartialSnapshot, | ||||||
| @@ -82,6 +80,8 @@ class HassioSnapshots extends LitElement { | |||||||
|     { slug: "addons/local", name: "Local add-ons", checked: true }, |     { slug: "addons/local", name: "Local add-ons", checked: true }, | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _creatingSnapshot = false; | ||||||
|  |  | ||||||
|   @internalProperty() private _error = ""; |   @internalProperty() private _error = ""; | ||||||
|  |  | ||||||
|   public async refreshData() { |   public async refreshData() { | ||||||
| @@ -192,9 +192,12 @@ class HassioSnapshots extends LitElement { | |||||||
|                   : undefined} |                   : undefined} | ||||||
|               </div> |               </div> | ||||||
|               <div class="card-actions"> |               <div class="card-actions"> | ||||||
|                 <ha-progress-button @click=${this._createSnapshot}> |                 <mwc-button | ||||||
|  |                   .disabled=${this._creatingSnapshot} | ||||||
|  |                   @click=${this._createSnapshot} | ||||||
|  |                 > | ||||||
|                   Create |                   Create | ||||||
|                 </ha-progress-button> |                 </mwc-button> | ||||||
|               </div> |               </div> | ||||||
|             </ha-card> |             </ha-card> | ||||||
|           </div> |           </div> | ||||||
| @@ -227,7 +230,7 @@ class HassioSnapshots extends LitElement { | |||||||
|                           .icon=${snapshot.type === "full" |                           .icon=${snapshot.type === "full" | ||||||
|                             ? mdiPackageVariantClosed |                             ? mdiPackageVariantClosed | ||||||
|                             : mdiPackageVariant} |                             : mdiPackageVariant} | ||||||
|                           icon-class="snapshot" |                           .icon-class="snapshot" | ||||||
|                         ></hassio-card-content> |                         ></hassio-card-content> | ||||||
|                       </div> |                       </div> | ||||||
|                     </ha-card> |                     </ha-card> | ||||||
| @@ -290,20 +293,17 @@ class HassioSnapshots extends LitElement { | |||||||
|       this._snapshots = await fetchHassioSnapshots(this.hass); |       this._snapshots = await fetchHassioSnapshots(this.hass); | ||||||
|       this._snapshots.sort((a, b) => (a.date < b.date ? 1 : -1)); |       this._snapshots.sort((a, b) => (a.date < b.date ? 1 : -1)); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       this._error = extractApiErrorMessage(err); |       this._error = err.message; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _createSnapshot(ev: CustomEvent): Promise<void> { |   private async _createSnapshot() { | ||||||
|     const button = ev.currentTarget as any; |  | ||||||
|     button.progress = true; |  | ||||||
|  |  | ||||||
|     this._error = ""; |     this._error = ""; | ||||||
|     if (this._snapshotHasPassword && !this._snapshotPassword.length) { |     if (this._snapshotHasPassword && !this._snapshotPassword.length) { | ||||||
|       this._error = "Please enter a password."; |       this._error = "Please enter a password."; | ||||||
|       button.progress = false; |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |     this._creatingSnapshot = true; | ||||||
|     await this.updateComplete; |     await this.updateComplete; | ||||||
|  |  | ||||||
|     const name = |     const name = | ||||||
| @@ -343,9 +343,10 @@ class HassioSnapshots extends LitElement { | |||||||
|       this._updateSnapshots(); |       this._updateSnapshots(); | ||||||
|       fireEvent(this, "hass-api-called", { success: true, response: null }); |       fireEvent(this, "hass-api-called", { success: true, response: null }); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       this._error = extractApiErrorMessage(err); |       this._error = err.message; | ||||||
|  |     } finally { | ||||||
|  |       this._creatingSnapshot = false; | ||||||
|     } |     } | ||||||
|     button.progress = false; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _computeDetails(snapshot: HassioSnapshot) { |   private _computeDetails(snapshot: HassioSnapshot) { | ||||||
|   | |||||||
| @@ -1,29 +1,18 @@ | |||||||
| import "@material/mwc-button"; | import "@material/mwc-button"; | ||||||
| import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; |  | ||||||
| import "@material/mwc-list/mwc-list-item"; |  | ||||||
| import { mdiDotsVertical } from "@mdi/js"; |  | ||||||
| import { safeDump } from "js-yaml"; |  | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import memoizeOne from "memoize-one"; | import "../../../src/components/buttons/ha-call-api-button"; | ||||||
| import { atLeastVersion } from "../../../src/common/config/version"; |  | ||||||
| import "../../../src/components/buttons/ha-progress-button"; |  | ||||||
| import "../../../src/components/ha-button-menu"; |  | ||||||
| import "../../../src/components/ha-card"; |  | ||||||
| import "../../../src/components/ha-settings-row"; |  | ||||||
| import { extractApiErrorMessage } from "../../../src/data/hassio/common"; |  | ||||||
| import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware"; | import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware"; | ||||||
| import { | import { | ||||||
|   changeHostOptions, |   changeHostOptions, | ||||||
|   configSyncOS, |  | ||||||
|   fetchHassioHostInfo, |   fetchHassioHostInfo, | ||||||
|   HassioHassOSInfo, |   HassioHassOSInfo, | ||||||
|   HassioHostInfo as HassioHostInfoType, |   HassioHostInfo as HassioHostInfoType, | ||||||
| @@ -31,10 +20,6 @@ import { | |||||||
|   shutdownHost, |   shutdownHost, | ||||||
|   updateOS, |   updateOS, | ||||||
| } from "../../../src/data/hassio/host"; | } from "../../../src/data/hassio/host"; | ||||||
| import { |  | ||||||
|   fetchNetworkInfo, |  | ||||||
|   NetworkInfo, |  | ||||||
| } from "../../../src/data/hassio/network"; |  | ||||||
| import { HassioInfo } from "../../../src/data/hassio/supervisor"; | import { HassioInfo } from "../../../src/data/hassio/supervisor"; | ||||||
| import { | import { | ||||||
|   showAlertDialog, |   showAlertDialog, | ||||||
| @@ -44,7 +29,6 @@ import { | |||||||
| import { haStyle } from "../../../src/resources/styles"; | import { haStyle } from "../../../src/resources/styles"; | ||||||
| import { HomeAssistant } from "../../../src/types"; | import { HomeAssistant } from "../../../src/types"; | ||||||
| import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown"; | import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown"; | ||||||
| import { showNetworkDialog } from "../dialogs/network/show-dialog-network"; |  | ||||||
| import { hassioStyle } from "../resources/hassio-style"; | import { hassioStyle } from "../resources/hassio-style"; | ||||||
|  |  | ||||||
| @customElement("hassio-host-info") | @customElement("hassio-host-info") | ||||||
| @@ -57,179 +41,164 @@ class HassioHostInfo extends LitElement { | |||||||
|  |  | ||||||
|   @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; |   @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; | ||||||
|  |  | ||||||
|   @internalProperty() public _networkInfo?: NetworkInfo; |   @internalProperty() private _errors?: string; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult | void { |   public render(): TemplateResult | void { | ||||||
|     const primaryIpAddress = this.hostInfo.features.includes("network") |  | ||||||
|       ? this._primaryIpAddress(this._networkInfo!) |  | ||||||
|       : ""; |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-card header="Host System"> |       <ha-card> | ||||||
|         <div class="card-content"> |         <div class="card-content"> | ||||||
|           ${this.hostInfo.features.includes("hostname") |           <h2>Host system</h2> | ||||||
|             ? html`<ha-settings-row> |           <table class="info"> | ||||||
|                 <span slot="heading"> |             <tbody> | ||||||
|                   Hostname |               <tr> | ||||||
|                 </span> |                 <td>Hostname</td> | ||||||
|                 <span slot="description"> |                 <td>${this.hostInfo.hostname}</td> | ||||||
|                   ${this.hostInfo.hostname} |               </tr> | ||||||
|                 </span> |               <tr> | ||||||
|                 <mwc-button |                 <td>System</td> | ||||||
|                   title="Change the hostname" |                 <td>${this.hostInfo.operating_system}</td> | ||||||
|                   label="Change" |               </tr> | ||||||
|                   @click=${this._changeHostnameClicked} |  | ||||||
|                 > |  | ||||||
|                 </mwc-button> |  | ||||||
|               </ha-settings-row>` |  | ||||||
|             : ""} |  | ||||||
|           ${this.hostInfo.features.includes("network") && |  | ||||||
|           atLeastVersion(this.hass.config.version, 0, 115) |  | ||||||
|             ? html` <ha-settings-row> |  | ||||||
|                 <span slot="heading"> |  | ||||||
|                   IP address |  | ||||||
|                 </span> |  | ||||||
|                 <span slot="description"> |  | ||||||
|                   ${primaryIpAddress} |  | ||||||
|                 </span> |  | ||||||
|                 <mwc-button |  | ||||||
|                   title="Change the network" |  | ||||||
|                   label="Change" |  | ||||||
|                   @click=${this._changeNetworkClicked} |  | ||||||
|                 > |  | ||||||
|                 </mwc-button> |  | ||||||
|               </ha-settings-row>` |  | ||||||
|             : ""} |  | ||||||
|  |  | ||||||
|           <ha-settings-row> |  | ||||||
|             <span slot="heading"> |  | ||||||
|               Operating system |  | ||||||
|             </span> |  | ||||||
|             <span slot="description"> |  | ||||||
|               ${this.hostInfo.operating_system} |  | ||||||
|             </span> |  | ||||||
|             ${this.hostInfo.version !== this.hostInfo.version_latest && |  | ||||||
|             this.hostInfo.features.includes("hassos") |  | ||||||
|               ? html` |  | ||||||
|                   <ha-progress-button |  | ||||||
|                     title="Update the host OS" |  | ||||||
|                     @click=${this._osUpdate} |  | ||||||
|                   > |  | ||||||
|                     Update |  | ||||||
|                   </ha-progress-button> |  | ||||||
|                 ` |  | ||||||
|               : ""} |  | ||||||
|           </ha-settings-row> |  | ||||||
|               ${!this.hostInfo.features.includes("hassos") |               ${!this.hostInfo.features.includes("hassos") | ||||||
|             ? html`<ha-settings-row> |                 ? html`<tr> | ||||||
|                 <span slot="heading"> |                     <td>Docker version</td> | ||||||
|                   Docker version |                     <td>${this.hassioInfo.docker}</td> | ||||||
|                 </span> |                   </tr>` | ||||||
|                 <span slot="description"> |  | ||||||
|                   ${this.hassioInfo.docker} |  | ||||||
|                 </span> |  | ||||||
|               </ha-settings-row>` |  | ||||||
|                 : ""} |                 : ""} | ||||||
|               ${this.hostInfo.deployment |               ${this.hostInfo.deployment | ||||||
|             ? html`<ha-settings-row> |                 ? html` | ||||||
|                 <span slot="heading"> |                     <tr> | ||||||
|                   Deployment |                       <td>Deployment</td> | ||||||
|                 </span> |                       <td>${this.hostInfo.deployment}</td> | ||||||
|                 <span slot="description"> |                     </tr> | ||||||
|                   ${this.hostInfo.deployment} |                   ` | ||||||
|                 </span> |                 : ""} | ||||||
|               </ha-settings-row>` |             </tbody> | ||||||
|  |           </table> | ||||||
|  |           <mwc-button raised @click=${this._showHardware} class="info"> | ||||||
|  |             Hardware | ||||||
|  |           </mwc-button> | ||||||
|  |           ${this.hostInfo.features.includes("hostname") | ||||||
|  |             ? html` | ||||||
|  |                 <mwc-button | ||||||
|  |                   raised | ||||||
|  |                   @click=${this._changeHostnameClicked} | ||||||
|  |                   class="info" | ||||||
|  |                 > | ||||||
|  |                   Change hostname | ||||||
|  |                 </mwc-button> | ||||||
|  |               ` | ||||||
|  |             : ""} | ||||||
|  |           ${this._errors | ||||||
|  |             ? html` <div class="errors">Error: ${this._errors}</div> ` | ||||||
|             : ""} |             : ""} | ||||||
|         </div> |         </div> | ||||||
|         <div class="card-actions"> |         <div class="card-actions"> | ||||||
|           ${this.hostInfo.features.includes("reboot") |           ${this.hostInfo.features.includes("reboot") | ||||||
|             ? html` |             ? html` | ||||||
|                 <ha-progress-button |                 <mwc-button class="warning" @click=${this._rebootHost} | ||||||
|                   title="Reboot the host OS" |                   >Reboot</mwc-button | ||||||
|                   class="warning" |  | ||||||
|                   @click=${this._hostReboot} |  | ||||||
|                 > |                 > | ||||||
|                   Reboot |  | ||||||
|                 </ha-progress-button> |  | ||||||
|               ` |               ` | ||||||
|             : ""} |             : ""} | ||||||
|           ${this.hostInfo.features.includes("shutdown") |           ${this.hostInfo.features.includes("shutdown") | ||||||
|             ? html` |             ? html` | ||||||
|                 <ha-progress-button |                 <mwc-button class="warning" @click=${this._shutdownHost} | ||||||
|                   title="Shutdown the host OS" |                   >Shutdown</mwc-button | ||||||
|                   class="warning" |  | ||||||
|                   @click=${this._hostShutdown} |  | ||||||
|                 > |                 > | ||||||
|                   Shutdown |  | ||||||
|                 </ha-progress-button> |  | ||||||
|               ` |               ` | ||||||
|             : ""} |             : ""} | ||||||
|  |  | ||||||
|           <ha-button-menu |  | ||||||
|             corner="BOTTOM_START" |  | ||||||
|             @action=${this._handleMenuAction} |  | ||||||
|           > |  | ||||||
|             <mwc-icon-button slot="trigger"> |  | ||||||
|               <ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon> |  | ||||||
|             </mwc-icon-button> |  | ||||||
|             <mwc-list-item title="Show a list of hardware"> |  | ||||||
|               Hardware |  | ||||||
|             </mwc-list-item> |  | ||||||
|           ${this.hostInfo.features.includes("hassos") |           ${this.hostInfo.features.includes("hassos") | ||||||
|               ? html`<mwc-list-item |             ? html` | ||||||
|  |                 <ha-call-api-button | ||||||
|  |                   class="warning" | ||||||
|  |                   .hass=${this.hass} | ||||||
|  |                   path="hassio/os/config/sync" | ||||||
|                   title="Load HassOS configs or updates from USB" |                   title="Load HassOS configs or updates from USB" | ||||||
|  |                   >Import from USB</ha-call-api-button | ||||||
|                 > |                 > | ||||||
|                   Import from USB |               ` | ||||||
|                 </mwc-list-item>` |             : ""} | ||||||
|  |           ${this.hostInfo.version !== this.hostInfo.version_latest | ||||||
|  |             ? html` <mwc-button @click=${this._updateOS}>Update</mwc-button> ` | ||||||
|             : ""} |             : ""} | ||||||
|           </ha-button-menu> |  | ||||||
|         </div> |         </div> | ||||||
|       </ha-card> |       </ha-card> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   static get styles(): CSSResult[] { | ||||||
|  |     return [ | ||||||
|  |       haStyle, | ||||||
|  |       hassioStyle, | ||||||
|  |       css` | ||||||
|  |         ha-card { | ||||||
|  |           height: 100%; | ||||||
|  |           width: 100%; | ||||||
|  |         } | ||||||
|  |         .card-content { | ||||||
|  |           color: var(--primary-text-color); | ||||||
|  |           box-sizing: border-box; | ||||||
|  |           height: calc(100% - 47px); | ||||||
|  |         } | ||||||
|  |         .info { | ||||||
|  |           width: 100%; | ||||||
|  |         } | ||||||
|  |         .info td:nth-child(2) { | ||||||
|  |           text-align: right; | ||||||
|  |         } | ||||||
|  |         .errors { | ||||||
|  |           color: var(--error-color); | ||||||
|  |           margin-top: 16px; | ||||||
|  |         } | ||||||
|  |         mwc-button.info { | ||||||
|  |           max-width: calc(50% - 12px); | ||||||
|  |         } | ||||||
|  |         table.info { | ||||||
|  |           margin-bottom: 10px; | ||||||
|  |         } | ||||||
|  |         .warning { | ||||||
|  |           --mdc-theme-primary: var(--error-color); | ||||||
|  |         } | ||||||
|  |       `, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   protected firstUpdated(): void { |   protected firstUpdated(): void { | ||||||
|     this._loadData(); |     this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => { |   private _apiCalled(ev): void { | ||||||
|     if (!network_info) { |     if (ev.detail.success) { | ||||||
|       return ""; |       this._errors = undefined; | ||||||
|  |       return; | ||||||
|     } |     } | ||||||
|     return Object.keys(network_info?.interfaces) |  | ||||||
|       .map((device) => network_info.interfaces[device]) |  | ||||||
|       .find((device) => device.primary)?.ip_address; |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   private async _handleMenuAction(ev: CustomEvent<ActionDetail>) { |     const response = ev.detail.response; | ||||||
|     switch (ev.detail.index) { |  | ||||||
|       case 0: |     this._errors = | ||||||
|         await this._showHardware(); |       typeof response.body === "object" | ||||||
|         break; |         ? response.body.message || "Unknown error" | ||||||
|       case 1: |         : response.body; | ||||||
|         await this._importFromUSB(); |  | ||||||
|         break; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _showHardware(): Promise<void> { |   private async _showHardware(): Promise<void> { | ||||||
|     try { |     try { | ||||||
|       const content = await fetchHassioHardwareInfo(this.hass); |       const content = this._objectToMarkdown( | ||||||
|  |         await fetchHassioHardwareInfo(this.hass) | ||||||
|  |       ); | ||||||
|       showHassioMarkdownDialog(this, { |       showHassioMarkdownDialog(this, { | ||||||
|         title: "Hardware", |         title: "Hardware", | ||||||
|         content: `<pre>${safeDump(content, { indent: 2 })}</pre>`, |         content, | ||||||
|       }); |       }); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       showAlertDialog(this, { |       showHassioMarkdownDialog(this, { | ||||||
|         title: "Failed to get Hardware list", |         title: "Hardware", | ||||||
|         text: extractApiErrorMessage(err), |         content: "Error getting hardware info", | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _hostReboot(ev: CustomEvent): Promise<void> { |   private async _rebootHost(): Promise<void> { | ||||||
|     const button = ev.currentTarget as any; |  | ||||||
|     button.progress = true; |  | ||||||
|  |  | ||||||
|     const confirmed = await showConfirmationDialog(this, { |     const confirmed = await showConfirmationDialog(this, { | ||||||
|       title: "Reboot", |       title: "Reboot", | ||||||
|       text: "Are you sure you want to reboot the host?", |       text: "Are you sure you want to reboot the host?", | ||||||
| @@ -238,7 +207,6 @@ class HassioHostInfo extends LitElement { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (!confirmed) { |     if (!confirmed) { | ||||||
|       button.progress = false; |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -247,16 +215,12 @@ class HassioHostInfo extends LitElement { | |||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       showAlertDialog(this, { |       showAlertDialog(this, { | ||||||
|         title: "Failed to reboot", |         title: "Failed to reboot", | ||||||
|         text: extractApiErrorMessage(err), |         text: err.body.message, | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|     button.progress = false; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _hostShutdown(ev: CustomEvent): Promise<void> { |   private async _shutdownHost(): Promise<void> { | ||||||
|     const button = ev.currentTarget as any; |  | ||||||
|     button.progress = true; |  | ||||||
|  |  | ||||||
|     const confirmed = await showConfirmationDialog(this, { |     const confirmed = await showConfirmationDialog(this, { | ||||||
|       title: "Shutdown", |       title: "Shutdown", | ||||||
|       text: "Are you sure you want to shutdown the host?", |       text: "Are you sure you want to shutdown the host?", | ||||||
| @@ -265,7 +229,6 @@ class HassioHostInfo extends LitElement { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (!confirmed) { |     if (!confirmed) { | ||||||
|       button.progress = false; |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -274,16 +237,12 @@ class HassioHostInfo extends LitElement { | |||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       showAlertDialog(this, { |       showAlertDialog(this, { | ||||||
|         title: "Failed to shutdown", |         title: "Failed to shutdown", | ||||||
|         text: extractApiErrorMessage(err), |         text: err.body.message, | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|     button.progress = false; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _osUpdate(ev: CustomEvent): Promise<void> { |   private async _updateOS(): Promise<void> { | ||||||
|     const button = ev.currentTarget as any; |  | ||||||
|     button.progress = true; |  | ||||||
|  |  | ||||||
|     const confirmed = await showConfirmationDialog(this, { |     const confirmed = await showConfirmationDialog(this, { | ||||||
|       title: "Update", |       title: "Update", | ||||||
|       text: "Are you sure you want to update the OS?", |       text: "Are you sure you want to update the OS?", | ||||||
| @@ -292,7 +251,6 @@ class HassioHostInfo extends LitElement { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (!confirmed) { |     if (!confirmed) { | ||||||
|       button.progress = false; |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -301,17 +259,30 @@ class HassioHostInfo extends LitElement { | |||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       showAlertDialog(this, { |       showAlertDialog(this, { | ||||||
|         title: "Failed to update", |         title: "Failed to update", | ||||||
|         text: extractApiErrorMessage(err), |         text: err.body.message, | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|     button.progress = false; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _changeNetworkClicked(): Promise<void> { |   private _objectToMarkdown(obj, indent = ""): string { | ||||||
|     showNetworkDialog(this, { |     let data = ""; | ||||||
|       network: this._networkInfo!, |     Object.keys(obj).forEach((key) => { | ||||||
|       loadData: () => this._loadData(), |       if (typeof obj[key] !== "object") { | ||||||
|  |         data += `${indent}- ${key}: ${obj[key]}\n`; | ||||||
|  |       } else { | ||||||
|  |         data += `${indent}- ${key}:\n`; | ||||||
|  |         if (Array.isArray(obj[key])) { | ||||||
|  |           if (obj[key].length) { | ||||||
|  |             data += | ||||||
|  |               `${indent}    - ` + obj[key].join(`\n${indent}    - `) + "\n"; | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           data += this._objectToMarkdown(obj[key], `    ${indent}`); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     return data; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _changeHostnameClicked(): Promise<void> { |   private async _changeHostnameClicked(): Promise<void> { | ||||||
| @@ -330,83 +301,11 @@ class HassioHostInfo extends LitElement { | |||||||
|       } catch (err) { |       } catch (err) { | ||||||
|         showAlertDialog(this, { |         showAlertDialog(this, { | ||||||
|           title: "Setting hostname failed", |           title: "Setting hostname failed", | ||||||
|           text: extractApiErrorMessage(err), |           text: err.body.message, | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _importFromUSB(): Promise<void> { |  | ||||||
|     try { |  | ||||||
|       await configSyncOS(this.hass); |  | ||||||
|       this.hostInfo = await fetchHassioHostInfo(this.hass); |  | ||||||
|     } catch (err) { |  | ||||||
|       showAlertDialog(this, { |  | ||||||
|         title: "Failed to import from USB", |  | ||||||
|         text: extractApiErrorMessage(err), |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _loadData(): Promise<void> { |  | ||||||
|     this._networkInfo = await fetchNetworkInfo(this.hass); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult[] { |  | ||||||
|     return [ |  | ||||||
|       haStyle, |  | ||||||
|       hassioStyle, |  | ||||||
|       css` |  | ||||||
|         ha-card { |  | ||||||
|           height: 100%; |  | ||||||
|           justify-content: space-between; |  | ||||||
|           flex-direction: column; |  | ||||||
|           display: flex; |  | ||||||
|         } |  | ||||||
|         .card-actions { |  | ||||||
|           height: 48px; |  | ||||||
|           border-top: none; |  | ||||||
|           display: flex; |  | ||||||
|           justify-content: space-between; |  | ||||||
|           align-items: center; |  | ||||||
|         } |  | ||||||
|         ha-settings-row { |  | ||||||
|           padding: 0; |  | ||||||
|           height: 54px; |  | ||||||
|           width: 100%; |  | ||||||
|         } |  | ||||||
|         ha-settings-row[three-line] { |  | ||||||
|           height: 74px; |  | ||||||
|         } |  | ||||||
|         ha-settings-row > span[slot="description"] { |  | ||||||
|           white-space: normal; |  | ||||||
|           color: var(--secondary-text-color); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .warning { |  | ||||||
|           --mdc-theme-primary: var(--error-color); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         ha-button-menu { |  | ||||||
|           color: var(--secondary-text-color); |  | ||||||
|           --mdc-menu-min-width: 200px; |  | ||||||
|         } |  | ||||||
|         @media (min-width: 563px) { |  | ||||||
|           paper-listbox { |  | ||||||
|             max-height: 150px; |  | ||||||
|             overflow: auto; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         paper-item { |  | ||||||
|           cursor: pointer; |  | ||||||
|           min-height: 35px; |  | ||||||
|         } |  | ||||||
|         mwc-list-item ha-svg-icon { |  | ||||||
|           color: var(--secondary-text-color); |  | ||||||
|         } |  | ||||||
|       `, |  | ||||||
|     ]; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import "@material/mwc-button"; | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResult, |   CSSResult, | ||||||
| @@ -5,29 +6,26 @@ import { | |||||||
|   html, |   html, | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import "../../../src/components/buttons/ha-progress-button"; | import { fireEvent } from "../../../src/common/dom/fire_event"; | ||||||
|  | import "../../../src/components/buttons/ha-call-api-button"; | ||||||
| import "../../../src/components/ha-card"; | import "../../../src/components/ha-card"; | ||||||
| import "../../../src/components/ha-settings-row"; |  | ||||||
| import "../../../src/components/ha-switch"; |  | ||||||
| import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host"; |  | ||||||
| import { | import { | ||||||
|   HassioSupervisorInfo as HassioSupervisorInfoType, |   HassioSupervisorInfo as HassioSupervisorInfoType, | ||||||
|   reloadSupervisor, |  | ||||||
|   setSupervisorOption, |   setSupervisorOption, | ||||||
|   SupervisorOptions, |   SupervisorOptions, | ||||||
|   updateSupervisor, |  | ||||||
|   fetchHassioSupervisorInfo, |  | ||||||
| } from "../../../src/data/hassio/supervisor"; | } from "../../../src/data/hassio/supervisor"; | ||||||
|  | import "../../../src/components/ha-switch"; | ||||||
| import { | import { | ||||||
|   showAlertDialog, |  | ||||||
|   showConfirmationDialog, |   showConfirmationDialog, | ||||||
|  |   showAlertDialog, | ||||||
| } from "../../../src/dialogs/generic/show-dialog-box"; | } from "../../../src/dialogs/generic/show-dialog-box"; | ||||||
|  | import "../../../src/components/ha-settings-row"; | ||||||
| import { haStyle } from "../../../src/resources/styles"; | import { haStyle } from "../../../src/resources/styles"; | ||||||
| import { HomeAssistant } from "../../../src/types"; | import { HomeAssistant } from "../../../src/types"; | ||||||
| import { hassioStyle } from "../resources/hassio-style"; | import { hassioStyle } from "../resources/hassio-style"; | ||||||
| import { extractApiErrorMessage } from "../../../src/data/hassio/common"; |  | ||||||
|  |  | ||||||
| @customElement("hassio-supervisor-info") | @customElement("hassio-supervisor-info") | ||||||
| class HassioSupervisorInfo extends LitElement { | class HassioSupervisorInfo extends LitElement { | ||||||
| @@ -35,120 +33,149 @@ class HassioSupervisorInfo extends LitElement { | |||||||
|  |  | ||||||
|   @property() public supervisorInfo!: HassioSupervisorInfoType; |   @property() public supervisorInfo!: HassioSupervisorInfoType; | ||||||
|  |  | ||||||
|   @property() public hostInfo!: HassioHostInfoType; |   @internalProperty() private _errors?: string; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult | void { |   public render(): TemplateResult | void { | ||||||
|     return html` |     return html` | ||||||
|       <ha-card header="Supervisor"> |       <ha-card> | ||||||
|         <div class="card-content"> |         <div class="card-content"> | ||||||
|           <ha-settings-row> |           <h2>Supervisor</h2> | ||||||
|             <span slot="heading"> |           <table class="info"> | ||||||
|               Version |             <tbody> | ||||||
|             </span> |               <tr> | ||||||
|             <span slot="description"> |                 <td>Version</td> | ||||||
|               ${this.supervisorInfo.version} |                 <td>${this.supervisorInfo.version}</td> | ||||||
|             </span> |               </tr> | ||||||
|           </ha-settings-row> |               <tr> | ||||||
|           <ha-settings-row> |                 <td>Latest version</td> | ||||||
|             <span slot="heading"> |                 <td>${this.supervisorInfo.version_latest}</td> | ||||||
|               Newest version |               </tr> | ||||||
|             </span> |               ${this.supervisorInfo.channel !== "stable" | ||||||
|             <span slot="description"> |  | ||||||
|               ${this.supervisorInfo.version_latest} |  | ||||||
|             </span> |  | ||||||
|             ${this.supervisorInfo.version !== this.supervisorInfo.version_latest |  | ||||||
|                 ? html` |                 ? html` | ||||||
|                   <ha-progress-button |                     <tr> | ||||||
|                     title="Update the supervisor" |                       <td>Channel</td> | ||||||
|                     @click=${this._supervisorUpdate} |                       <td>${this.supervisorInfo.channel}</td> | ||||||
|                   > |                     </tr> | ||||||
|                     Update |  | ||||||
|                   </ha-progress-button> |  | ||||||
|                   ` |                   ` | ||||||
|                 : ""} |                 : ""} | ||||||
|           </ha-settings-row> |             </tbody> | ||||||
|  |           </table> | ||||||
|  |           <div class="options"> | ||||||
|             <ha-settings-row> |             <ha-settings-row> | ||||||
|               <span slot="heading"> |               <span slot="heading"> | ||||||
|               Channel |                 Share Diagnostics | ||||||
|               </span> |               </span> | ||||||
|               <span slot="description"> |               <span slot="description"> | ||||||
|               ${this.supervisorInfo.channel} |  | ||||||
|             </span> |  | ||||||
|             ${this.supervisorInfo.channel === "beta" |  | ||||||
|               ? html` |  | ||||||
|                   <ha-progress-button |  | ||||||
|                     @click=${this._toggleBeta} |  | ||||||
|                     title="Get stable updates for Home Assistant, supervisor and host" |  | ||||||
|                   > |  | ||||||
|                     Leave beta channel |  | ||||||
|                   </ha-progress-button> |  | ||||||
|                 ` |  | ||||||
|               : this.supervisorInfo.channel === "stable" |  | ||||||
|               ? html` |  | ||||||
|                   <ha-progress-button |  | ||||||
|                     @click=${this._toggleBeta} |  | ||||||
|                     title="Get beta updates for Home Assistant (RCs), supervisor and host" |  | ||||||
|                   > |  | ||||||
|                     Join beta channel |  | ||||||
|                   </ha-progress-button> |  | ||||||
|                 ` |  | ||||||
|               : ""} |  | ||||||
|           </ha-settings-row> |  | ||||||
|  |  | ||||||
|           ${this.supervisorInfo?.supported |  | ||||||
|             ? html` <ha-settings-row three-line> |  | ||||||
|                 <span slot="heading"> |  | ||||||
|                   Share diagnostics |  | ||||||
|                 </span> |  | ||||||
|                 <div slot="description" class="diagnostics-description"> |  | ||||||
|                 Share crash reports and diagnostic information. |                 Share crash reports and diagnostic information. | ||||||
|                 <button |                 <button | ||||||
|                   class="link" |                   class="link" | ||||||
|                     title="Show more information about this" |  | ||||||
|                   @click=${this._diagnosticsInformationDialog} |                   @click=${this._diagnosticsInformationDialog} | ||||||
|                 > |                 > | ||||||
|                   Learn more |                   Learn more | ||||||
|                 </button> |                 </button> | ||||||
|                 </div> |               </span> | ||||||
|               <ha-switch |               <ha-switch | ||||||
|                   haptic |  | ||||||
|                 .checked=${this.supervisorInfo.diagnostics} |                 .checked=${this.supervisorInfo.diagnostics} | ||||||
|                 @change=${this._toggleDiagnostics} |                 @change=${this._toggleDiagnostics} | ||||||
|               ></ha-switch> |               ></ha-switch> | ||||||
|               </ha-settings-row>` |             </ha-settings-row> | ||||||
|             : html`<div class="error"> |           </div> | ||||||
|                 You are running an unsupported installation. |           ${this._errors | ||||||
|                 <a |             ? html` <div class="errors">Error: ${this._errors}</div> ` | ||||||
|                   href="https://github.com/home-assistant/architecture/blob/master/adr/${this.hostInfo.features.includes( |             : ""} | ||||||
|                     "hassos" |  | ||||||
|                   ) |  | ||||||
|                     ? "0015-home-assistant-os.md" |  | ||||||
|                     : "0014-home-assistant-supervised.md"}" |  | ||||||
|                   target="_blank" |  | ||||||
|                   rel="noreferrer" |  | ||||||
|                   title="Learn more about how you can make your system compliant" |  | ||||||
|                 > |  | ||||||
|                   Learn More |  | ||||||
|                 </a> |  | ||||||
|               </div>`} |  | ||||||
|         </div> |         </div> | ||||||
|         <div class="card-actions"> |         <div class="card-actions"> | ||||||
|           <ha-progress-button |           <ha-call-api-button .hass=${this.hass} path="hassio/supervisor/reload" | ||||||
|             @click=${this._supervisorReload} |             >Reload</ha-call-api-button | ||||||
|             title="Reload parts of the supervisor." |  | ||||||
|           > |           > | ||||||
|             Reload |           ${this.supervisorInfo.version !== this.supervisorInfo.version_latest | ||||||
|           </ha-progress-button> |             ? html` | ||||||
|  |                 <ha-call-api-button | ||||||
|  |                   .hass=${this.hass} | ||||||
|  |                   path="hassio/supervisor/update" | ||||||
|  |                   >Update</ha-call-api-button | ||||||
|  |                 > | ||||||
|  |               ` | ||||||
|  |             : ""} | ||||||
|  |           ${this.supervisorInfo.channel === "beta" | ||||||
|  |             ? html` | ||||||
|  |                 <ha-call-api-button | ||||||
|  |                   .hass=${this.hass} | ||||||
|  |                   path="hassio/supervisor/options" | ||||||
|  |                   .data=${{ channel: "stable" }} | ||||||
|  |                   >Leave beta channel</ha-call-api-button | ||||||
|  |                 > | ||||||
|  |               ` | ||||||
|  |             : ""} | ||||||
|  |           ${this.supervisorInfo.channel === "stable" | ||||||
|  |             ? html` | ||||||
|  |                 <mwc-button | ||||||
|  |                   @click=${this._joinBeta} | ||||||
|  |                   class="warning" | ||||||
|  |                   title="Get beta updates for Home Assistant (RCs), supervisor and host" | ||||||
|  |                   >Join beta channel</mwc-button | ||||||
|  |                 > | ||||||
|  |               ` | ||||||
|  |             : ""} | ||||||
|         </div> |         </div> | ||||||
|       </ha-card> |       </ha-card> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _toggleBeta(ev: CustomEvent): Promise<void> { |   static get styles(): CSSResult[] { | ||||||
|     const button = ev.currentTarget as any; |     return [ | ||||||
|     button.progress = true; |       haStyle, | ||||||
|  |       hassioStyle, | ||||||
|  |       css` | ||||||
|  |         ha-card { | ||||||
|  |           height: 100%; | ||||||
|  |           width: 100%; | ||||||
|  |         } | ||||||
|  |         .card-content { | ||||||
|  |           color: var(--primary-text-color); | ||||||
|  |           box-sizing: border-box; | ||||||
|  |           height: calc(100% - 47px); | ||||||
|  |         } | ||||||
|  |         .info, | ||||||
|  |         .options { | ||||||
|  |           width: 100%; | ||||||
|  |         } | ||||||
|  |         .info td:nth-child(2) { | ||||||
|  |           text-align: right; | ||||||
|  |         } | ||||||
|  |         .errors { | ||||||
|  |           color: var(--error-color); | ||||||
|  |           margin-top: 16px; | ||||||
|  |         } | ||||||
|  |         ha-settings-row { | ||||||
|  |           padding: 0; | ||||||
|  |         } | ||||||
|  |         button.link { | ||||||
|  |           color: var(--primary-color); | ||||||
|  |         } | ||||||
|  |       `, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|     if (this.supervisorInfo.channel === "stable") { |   protected firstUpdated(): void { | ||||||
|  |     this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _apiCalled(ev): void { | ||||||
|  |     if (ev.detail.success) { | ||||||
|  |       this._errors = undefined; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const response = ev.detail.response; | ||||||
|  |  | ||||||
|  |     this._errors = | ||||||
|  |       typeof response.body === "object" | ||||||
|  |         ? response.body.message || "Unknown error" | ||||||
|  |         : response.body; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _joinBeta() { | ||||||
|     const confirmed = await showConfirmationDialog(this, { |     const confirmed = await showConfirmationDialog(this, { | ||||||
|       title: "WARNING", |       title: "WARNING", | ||||||
|       text: html` Beta releases are for testers and early adopters and can |       text: html` Beta releases are for testers and early adopters and can | ||||||
| @@ -170,71 +197,24 @@ class HassioSupervisorInfo extends LitElement { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (!confirmed) { |     if (!confirmed) { | ||||||
|         button.progress = false; |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     } |  | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       const data: Partial<SupervisorOptions> = { |       const data: SupervisorOptions = { channel: "beta" }; | ||||||
|         channel: this.supervisorInfo.channel === "stable" ? "beta" : "stable", |  | ||||||
|       }; |  | ||||||
|       await setSupervisorOption(this.hass, data); |       await setSupervisorOption(this.hass, data); | ||||||
|       await reloadSupervisor(this.hass); |       const eventdata = { | ||||||
|       this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass); |         success: true, | ||||||
|  |         response: undefined, | ||||||
|  |         path: "option", | ||||||
|  |       }; | ||||||
|  |       fireEvent(this, "hass-api-called", eventdata); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       showAlertDialog(this, { |       this._errors = `Error joining beta channel, ${err.body?.message || err}`; | ||||||
|         title: "Failed to set supervisor option", |  | ||||||
|         text: extractApiErrorMessage(err), |  | ||||||
|       }); |  | ||||||
|     } |     } | ||||||
|     button.progress = false; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _supervisorReload(ev: CustomEvent): Promise<void> { |   private async _diagnosticsInformationDialog() { | ||||||
|     const button = ev.currentTarget as any; |  | ||||||
|     button.progress = true; |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       await reloadSupervisor(this.hass); |  | ||||||
|       this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass); |  | ||||||
|     } catch (err) { |  | ||||||
|       showAlertDialog(this, { |  | ||||||
|         title: "Failed to reload the supervisor", |  | ||||||
|         text: extractApiErrorMessage(err), |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|     button.progress = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _supervisorUpdate(ev: CustomEvent): Promise<void> { |  | ||||||
|     const button = ev.currentTarget as any; |  | ||||||
|     button.progress = true; |  | ||||||
|  |  | ||||||
|     const confirmed = await showConfirmationDialog(this, { |  | ||||||
|       title: "Update supervisor", |  | ||||||
|       text: `Are you sure you want to upgrade supervisor to version ${this.supervisorInfo.version_latest}?`, |  | ||||||
|       confirmText: "update", |  | ||||||
|       dismissText: "cancel", |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     if (!confirmed) { |  | ||||||
|       button.progress = false; |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       await updateSupervisor(this.hass); |  | ||||||
|     } catch (err) { |  | ||||||
|       showAlertDialog(this, { |  | ||||||
|         title: "Failed to update the supervisor", |  | ||||||
|         text: extractApiErrorMessage(err), |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|     button.progress = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _diagnosticsInformationDialog(): Promise<void> { |  | ||||||
|     await showAlertDialog(this, { |     await showAlertDialog(this, { | ||||||
|       title: "Help Improve Home Assistant", |       title: "Help Improve Home Assistant", | ||||||
|       text: html`Would you want to automatically share crash reports and |       text: html`Would you want to automatically share crash reports and | ||||||
| @@ -244,61 +224,29 @@ class HassioSupervisorInfo extends LitElement { | |||||||
|         accessible to the Home Assistant Core team and will not be shared with |         accessible to the Home Assistant Core team and will not be shared with | ||||||
|         others. |         others. | ||||||
|         <br /><br /> |         <br /><br /> | ||||||
|         The data does not include any private/sensitive information and you can |         The data does not include any private/sensetive information and you can | ||||||
|         disable this in settings at any time you want.`, |         disable this in settings at any time you want.`, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _toggleDiagnostics(): Promise<void> { |   private async _toggleDiagnostics() { | ||||||
|     try { |     try { | ||||||
|       const data: SupervisorOptions = { |       const data: SupervisorOptions = { | ||||||
|         diagnostics: !this.supervisorInfo?.diagnostics, |         diagnostics: !this.supervisorInfo?.diagnostics, | ||||||
|       }; |       }; | ||||||
|       await setSupervisorOption(this.hass, data); |       await setSupervisorOption(this.hass, data); | ||||||
|  |       const eventdata = { | ||||||
|  |         success: true, | ||||||
|  |         response: undefined, | ||||||
|  |         path: "option", | ||||||
|  |       }; | ||||||
|  |       fireEvent(this, "hass-api-called", eventdata); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       showAlertDialog(this, { |       this._errors = `Error changing supervisor setting, ${ | ||||||
|         title: "Failed to set supervisor option", |         err.body?.message || err | ||||||
|         text: extractApiErrorMessage(err), |       }`; | ||||||
|       }); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResult[] { |  | ||||||
|     return [ |  | ||||||
|       haStyle, |  | ||||||
|       hassioStyle, |  | ||||||
|       css` |  | ||||||
|         ha-card { |  | ||||||
|           height: 100%; |  | ||||||
|           justify-content: space-between; |  | ||||||
|           flex-direction: column; |  | ||||||
|           display: flex; |  | ||||||
|         } |  | ||||||
|         .card-actions { |  | ||||||
|           height: 48px; |  | ||||||
|           border-top: none; |  | ||||||
|           display: flex; |  | ||||||
|           justify-content: space-between; |  | ||||||
|           align-items: center; |  | ||||||
|         } |  | ||||||
|         button.link { |  | ||||||
|           color: var(--primary-color); |  | ||||||
|         } |  | ||||||
|         ha-settings-row { |  | ||||||
|           padding: 0; |  | ||||||
|           height: 54px; |  | ||||||
|           width: 100%; |  | ||||||
|         } |  | ||||||
|         ha-settings-row[three-line] { |  | ||||||
|           height: 74px; |  | ||||||
|         } |  | ||||||
|         ha-settings-row > div[slot="description"] { |  | ||||||
|           white-space: normal; |  | ||||||
|           color: var(--secondary-text-color); |  | ||||||
|         } |  | ||||||
|       `, |  | ||||||
|     ]; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -7,14 +7,12 @@ import { | |||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import "../../../src/components/buttons/ha-progress-button"; |  | ||||||
| import "../../../src/components/ha-card"; | import "../../../src/components/ha-card"; | ||||||
| import { extractApiErrorMessage } from "../../../src/data/hassio/common"; |  | ||||||
| import { fetchHassioLogs } from "../../../src/data/hassio/supervisor"; | import { fetchHassioLogs } from "../../../src/data/hassio/supervisor"; | ||||||
| import "../../../src/layouts/hass-loading-screen"; | import "../../../src/layouts/hass-loading-screen"; | ||||||
| import { haStyle } from "../../../src/resources/styles"; | import { haStyle } from "../../../src/resources/styles"; | ||||||
| @@ -69,7 +67,7 @@ class HassioSupervisorLog extends LitElement { | |||||||
|     await this._loadData(); |     await this._loadData(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected render(): TemplateResult | void { |   public render(): TemplateResult | void { | ||||||
|     return html` |     return html` | ||||||
|       <ha-card> |       <ha-card> | ||||||
|         ${this._error ? html` <div class="errors">${this._error}</div> ` : ""} |         ${this._error ? html` <div class="errors">${this._error}</div> ` : ""} | ||||||
| @@ -104,49 +102,18 @@ class HassioSupervisorLog extends LitElement { | |||||||
|             : html`<hass-loading-screen no-toolbar></hass-loading-screen>`} |             : html`<hass-loading-screen no-toolbar></hass-loading-screen>`} | ||||||
|         </div> |         </div> | ||||||
|         <div class="card-actions"> |         <div class="card-actions"> | ||||||
|           <ha-progress-button @click=${this._refresh}> |           <mwc-button @click=${this._refresh}>Refresh</mwc-button> | ||||||
|             Refresh |  | ||||||
|           </ha-progress-button> |  | ||||||
|         </div> |         </div> | ||||||
|       </ha-card> |       </ha-card> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _setLogProvider(ev): Promise<void> { |  | ||||||
|     const provider = ev.detail.item.getAttribute("provider"); |  | ||||||
|     this._selectedLogProvider = provider; |  | ||||||
|     this._loadData(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _refresh(ev: CustomEvent): Promise<void> { |  | ||||||
|     const button = ev.currentTarget as any; |  | ||||||
|     button.progress = true; |  | ||||||
|     await this._loadData(); |  | ||||||
|     button.progress = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _loadData(): Promise<void> { |  | ||||||
|     this._error = undefined; |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       this._content = await fetchHassioLogs( |  | ||||||
|         this.hass, |  | ||||||
|         this._selectedLogProvider |  | ||||||
|       ); |  | ||||||
|     } catch (err) { |  | ||||||
|       this._error = `Failed to get supervisor logs, ${extractApiErrorMessage( |  | ||||||
|         err |  | ||||||
|       )}`; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult[] { |   static get styles(): CSSResult[] { | ||||||
|     return [ |     return [ | ||||||
|       haStyle, |       haStyle, | ||||||
|       hassioStyle, |       hassioStyle, | ||||||
|       css` |       css` | ||||||
|         ha-card { |         ha-card { | ||||||
|           margin-top: 8px; |  | ||||||
|           width: 100%; |           width: 100%; | ||||||
|         } |         } | ||||||
|         pre { |         pre { | ||||||
| @@ -160,9 +127,38 @@ class HassioSupervisorLog extends LitElement { | |||||||
|           color: var(--error-color); |           color: var(--error-color); | ||||||
|           margin-bottom: 16px; |           margin-bottom: 16px; | ||||||
|         } |         } | ||||||
|  |         .card-content { | ||||||
|  |           padding-top: 0px; | ||||||
|  |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private async _setLogProvider(ev): Promise<void> { | ||||||
|  |     const provider = ev.detail.item.getAttribute("provider"); | ||||||
|  |     this._selectedLogProvider = provider; | ||||||
|  |     await this._loadData(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _loadData(): Promise<void> { | ||||||
|  |     this._error = undefined; | ||||||
|  |     this._content = undefined; | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       this._content = await fetchHassioLogs( | ||||||
|  |         this.hass, | ||||||
|  |         this._selectedLogProvider | ||||||
|  |       ); | ||||||
|  |     } catch (err) { | ||||||
|  |       this._error = `Failed to get supervisor logs, ${ | ||||||
|  |         err.body?.message || err | ||||||
|  |       }`; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _refresh(): Promise<void> { | ||||||
|  |     await this._loadData(); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -12,8 +12,8 @@ import { | |||||||
|   HassioHostInfo, |   HassioHostInfo, | ||||||
| } from "../../../src/data/hassio/host"; | } from "../../../src/data/hassio/host"; | ||||||
| import { | import { | ||||||
|   HassioInfo, |  | ||||||
|   HassioSupervisorInfo, |   HassioSupervisorInfo, | ||||||
|  |   HassioInfo, | ||||||
| } from "../../../src/data/hassio/supervisor"; | } from "../../../src/data/hassio/supervisor"; | ||||||
| import "../../../src/layouts/hass-tabs-subpage"; | import "../../../src/layouts/hass-tabs-subpage"; | ||||||
| import { haStyle } from "../../../src/resources/styles"; | import { haStyle } from "../../../src/resources/styles"; | ||||||
| @@ -40,7 +40,7 @@ class HassioSystem extends LitElement { | |||||||
|  |  | ||||||
|   @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; |   @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult | void { |   public render(): TemplateResult | void { | ||||||
|     return html` |     return html` | ||||||
|       <hass-tabs-subpage |       <hass-tabs-subpage | ||||||
|         .hass=${this.hass} |         .hass=${this.hass} | ||||||
| @@ -52,10 +52,10 @@ class HassioSystem extends LitElement { | |||||||
|       > |       > | ||||||
|         <span slot="header">System</span> |         <span slot="header">System</span> | ||||||
|         <div class="content"> |         <div class="content"> | ||||||
|  |           <h1>Information</h1> | ||||||
|           <div class="card-group"> |           <div class="card-group"> | ||||||
|             <hassio-supervisor-info |             <hassio-supervisor-info | ||||||
|               .hass=${this.hass} |               .hass=${this.hass} | ||||||
|               .hostInfo=${this.hostInfo} |  | ||||||
|               .supervisorInfo=${this.supervisorInfo} |               .supervisorInfo=${this.supervisorInfo} | ||||||
|             ></hassio-supervisor-info> |             ></hassio-supervisor-info> | ||||||
|             <hassio-host-info |             <hassio-host-info | ||||||
| @@ -65,6 +65,7 @@ class HassioSystem extends LitElement { | |||||||
|               .hassOsInfo=${this.hassOsInfo} |               .hassOsInfo=${this.hassOsInfo} | ||||||
|             ></hassio-host-info> |             ></hassio-host-info> | ||||||
|           </div> |           </div> | ||||||
|  |           <h1>System log</h1> | ||||||
|           <hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log> |           <hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log> | ||||||
|         </div> |         </div> | ||||||
|       </hass-tabs-subpage> |       </hass-tabs-subpage> | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								package.json
									
									
									
									
									
								
							| @@ -23,11 +23,8 @@ | |||||||
|   "license": "Apache-2.0", |   "license": "Apache-2.0", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@formatjs/intl-pluralrules": "^1.5.8", |     "@formatjs/intl-pluralrules": "^1.5.8", | ||||||
|     "@fullcalendar/common": "5.1.0", |     "@fullcalendar/core": "^5.0.0-beta.2", | ||||||
|     "@fullcalendar/core": "5.1.0", |     "@fullcalendar/daygrid": "^5.0.0-beta.2", | ||||||
|     "@fullcalendar/daygrid": "5.1.0", |  | ||||||
|     "@fullcalendar/interaction": "5.1.0", |  | ||||||
|     "@fullcalendar/list": "5.1.0", |  | ||||||
|     "@material/chips": "=8.0.0-canary.096a7a066.0", |     "@material/chips": "=8.0.0-canary.096a7a066.0", | ||||||
|     "@material/circular-progress": "=8.0.0-canary.a78ceb112.0", |     "@material/circular-progress": "=8.0.0-canary.a78ceb112.0", | ||||||
|     "@material/mwc-button": "^0.18.0", |     "@material/mwc-button": "^0.18.0", | ||||||
| @@ -44,8 +41,8 @@ | |||||||
|     "@material/mwc-tab": "^0.18.0", |     "@material/mwc-tab": "^0.18.0", | ||||||
|     "@material/mwc-tab-bar": "^0.18.0", |     "@material/mwc-tab-bar": "^0.18.0", | ||||||
|     "@material/top-app-bar": "=8.0.0-canary.096a7a066.0", |     "@material/top-app-bar": "=8.0.0-canary.096a7a066.0", | ||||||
|     "@mdi/js": "5.5.55", |     "@mdi/js": "5.4.55", | ||||||
|     "@mdi/svg": "5.5.55", |     "@mdi/svg": "5.4.55", | ||||||
|     "@polymer/app-layout": "^3.0.2", |     "@polymer/app-layout": "^3.0.2", | ||||||
|     "@polymer/app-route": "^3.0.2", |     "@polymer/app-route": "^3.0.2", | ||||||
|     "@polymer/app-storage": "^3.0.2", |     "@polymer/app-storage": "^3.0.2", | ||||||
| @@ -79,7 +76,6 @@ | |||||||
|     "@polymer/polymer": "3.1.0", |     "@polymer/polymer": "3.1.0", | ||||||
|     "@thomasloven/round-slider": "0.5.0", |     "@thomasloven/round-slider": "0.5.0", | ||||||
|     "@types/chromecast-caf-sender": "^1.0.3", |     "@types/chromecast-caf-sender": "^1.0.3", | ||||||
|     "@types/sortablejs": "^1.10.6", |  | ||||||
|     "@vaadin/vaadin-combo-box": "^5.0.10", |     "@vaadin/vaadin-combo-box": "^5.0.10", | ||||||
|     "@vaadin/vaadin-date-picker": "^4.0.7", |     "@vaadin/vaadin-date-picker": "^4.0.7", | ||||||
|     "@vue/web-component-wrapper": "^1.2.0", |     "@vue/web-component-wrapper": "^1.2.0", | ||||||
| @@ -89,7 +85,6 @@ | |||||||
|     "codemirror": "^5.49.0", |     "codemirror": "^5.49.0", | ||||||
|     "comlink": "^4.3.0", |     "comlink": "^4.3.0", | ||||||
|     "cpx": "^1.5.0", |     "cpx": "^1.5.0", | ||||||
|     "cropperjs": "^1.5.7", |  | ||||||
|     "deep-clone-simple": "^1.1.1", |     "deep-clone-simple": "^1.1.1", | ||||||
|     "deep-freeze": "^0.0.1", |     "deep-freeze": "^0.0.1", | ||||||
|     "es6-object-assign": "^1.1.0", |     "es6-object-assign": "^1.1.0", | ||||||
| @@ -115,7 +110,6 @@ | |||||||
|     "regenerator-runtime": "^0.13.2", |     "regenerator-runtime": "^0.13.2", | ||||||
|     "resize-observer-polyfill": "^1.5.1", |     "resize-observer-polyfill": "^1.5.1", | ||||||
|     "roboto-fontface": "^0.10.0", |     "roboto-fontface": "^0.10.0", | ||||||
|     "sortablejs": "^1.10.2", |  | ||||||
|     "superstruct": "^0.10.12", |     "superstruct": "^0.10.12", | ||||||
|     "unfetch": "^4.1.0", |     "unfetch": "^4.1.0", | ||||||
|     "vue": "^2.6.11", |     "vue": "^2.6.11", | ||||||
| @@ -151,7 +145,7 @@ | |||||||
|     "@types/leaflet-draw": "^1.0.1", |     "@types/leaflet-draw": "^1.0.1", | ||||||
|     "@types/marked": "^1.1.0", |     "@types/marked": "^1.1.0", | ||||||
|     "@types/memoize-one": "4.1.0", |     "@types/memoize-one": "4.1.0", | ||||||
|     "@types/mocha": "^7.0.2", |     "@types/mocha": "^5.2.6", | ||||||
|     "@types/resize-observer-browser": "^0.1.3", |     "@types/resize-observer-browser": "^0.1.3", | ||||||
|     "@types/webspeechapi": "^0.0.29", |     "@types/webspeechapi": "^0.0.29", | ||||||
|     "@typescript-eslint/eslint-plugin": "^2.28.0", |     "@typescript-eslint/eslint-plugin": "^2.28.0", | ||||||
| @@ -162,7 +156,7 @@ | |||||||
|     "eslint": "^6.8.0", |     "eslint": "^6.8.0", | ||||||
|     "eslint-config-airbnb-typescript": "^7.2.1", |     "eslint-config-airbnb-typescript": "^7.2.1", | ||||||
|     "eslint-config-prettier": "^6.10.1", |     "eslint-config-prettier": "^6.10.1", | ||||||
|     "eslint-import-resolver-webpack": "^0.12.2", |     "eslint-import-resolver-webpack": "^0.12.1", | ||||||
|     "eslint-plugin-disable": "^2.0.1", |     "eslint-plugin-disable": "^2.0.1", | ||||||
|     "eslint-plugin-import": "^2.20.2", |     "eslint-plugin-import": "^2.20.2", | ||||||
|     "eslint-plugin-lit": "^1.2.0", |     "eslint-plugin-lit": "^1.2.0", | ||||||
| @@ -185,7 +179,7 @@ | |||||||
|     "magic-string": "^0.25.7", |     "magic-string": "^0.25.7", | ||||||
|     "map-stream": "^0.0.7", |     "map-stream": "^0.0.7", | ||||||
|     "merge-stream": "^1.0.1", |     "merge-stream": "^1.0.1", | ||||||
|     "mocha": "^7.2.0", |     "mocha": "^6.0.2", | ||||||
|     "object-hash": "^2.0.3", |     "object-hash": "^2.0.3", | ||||||
|     "open": "^7.0.4", |     "open": "^7.0.4", | ||||||
|     "prettier": "^2.0.4", |     "prettier": "^2.0.4", | ||||||
| @@ -202,7 +196,7 @@ | |||||||
|     "systemjs": "^6.3.2", |     "systemjs": "^6.3.2", | ||||||
|     "terser-webpack-plugin": "^3.0.6", |     "terser-webpack-plugin": "^3.0.6", | ||||||
|     "ts-lit-plugin": "^1.2.0", |     "ts-lit-plugin": "^1.2.0", | ||||||
|     "ts-mocha": "^7.0.0", |     "ts-mocha": "^6.0.0", | ||||||
|     "typescript": "^3.8.3", |     "typescript": "^3.8.3", | ||||||
|     "vinyl-buffer": "^1.0.1", |     "vinyl-buffer": "^1.0.1", | ||||||
|     "vinyl-source-stream": "^2.0.0", |     "vinyl-source-stream": "^2.0.0", | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
									
									
									
									
								
							| @@ -2,7 +2,7 @@ from setuptools import setup, find_packages | |||||||
|  |  | ||||||
| setup( | setup( | ||||||
|     name="home-assistant-frontend", |     name="home-assistant-frontend", | ||||||
|     version="20200908.0", |     version="20200807.1", | ||||||
|     description="The Home Assistant frontend", |     description="The Home Assistant frontend", | ||||||
|     url="https://github.com/home-assistant/home-assistant-polymer", |     url="https://github.com/home-assistant/home-assistant-polymer", | ||||||
|     author="The Home Assistant Authors", |     author="The Home Assistant Authors", | ||||||
|   | |||||||
| @@ -1,9 +0,0 @@ | |||||||
| import { HomeAssistant } from "../../types"; |  | ||||||
|  |  | ||||||
| /** Return an array of domains with the service. */ |  | ||||||
| export const componentsWithService = ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   service: string |  | ||||||
| ): Array<string> => |  | ||||||
|   hass && |  | ||||||
|   Object.keys(hass.services).filter((key) => service in hass.services[key]); |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| import { HomeAssistant } from "../../types"; |  | ||||||
|  |  | ||||||
| /** Return if a service is loaded. */ |  | ||||||
| export const isServiceLoaded = ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   domain: string, |  | ||||||
|   service: string |  | ||||||
| ): boolean => |  | ||||||
|   hass && domain in hass.services && service in hass.services[domain]; |  | ||||||
| @@ -21,11 +21,6 @@ export default function relativeTime( | |||||||
|   const tense = delta >= 0 ? "past" : "future"; |   const tense = delta >= 0 ? "past" : "future"; | ||||||
|   delta = Math.abs(delta); |   delta = Math.abs(delta); | ||||||
|   let roundedDelta = Math.round(delta); |   let roundedDelta = Math.round(delta); | ||||||
|  |  | ||||||
|   if (roundedDelta === 0) { |  | ||||||
|     return localize("ui.components.relative_time.just_now"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let unit = "week"; |   let unit = "week"; | ||||||
|  |  | ||||||
|   for (let i = 0; i < tests.length; i++) { |   for (let i = 0; i < tests.length; i++) { | ||||||
|   | |||||||
| @@ -1,155 +0,0 @@ | |||||||
| import { UnsubscribeFunc } from "home-assistant-js-websocket"; |  | ||||||
| import { PropertyDeclaration, UpdatingElement } from "lit-element"; |  | ||||||
| import type { ClassElement } from "../../types"; |  | ||||||
|  |  | ||||||
| type Callback = (oldValue: any, newValue: any) => void; |  | ||||||
|  |  | ||||||
| class Storage { |  | ||||||
|   constructor() { |  | ||||||
|     window.addEventListener("storage", (ev: StorageEvent) => { |  | ||||||
|       if (ev.key && this.hasKey(ev.key)) { |  | ||||||
|         this._storage[ev.key] = ev.newValue |  | ||||||
|           ? JSON.parse(ev.newValue) |  | ||||||
|           : ev.newValue; |  | ||||||
|         if (this._listeners[ev.key]) { |  | ||||||
|           this._listeners[ev.key].forEach((listener) => |  | ||||||
|             listener( |  | ||||||
|               ev.oldValue ? JSON.parse(ev.oldValue) : ev.oldValue, |  | ||||||
|               this._storage[ev.key!] |  | ||||||
|             ) |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _storage: { [storageKey: string]: any } = {}; |  | ||||||
|  |  | ||||||
|   private _listeners: { |  | ||||||
|     [storageKey: string]: Callback[]; |  | ||||||
|   } = {}; |  | ||||||
|  |  | ||||||
|   public addFromStorage(storageKey: any): void { |  | ||||||
|     if (!this._storage[storageKey]) { |  | ||||||
|       const data = window.localStorage.getItem(storageKey); |  | ||||||
|       if (data) { |  | ||||||
|         this._storage[storageKey] = JSON.parse(data); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public subscribeChanges( |  | ||||||
|     storageKey: string, |  | ||||||
|     callback: Callback |  | ||||||
|   ): UnsubscribeFunc { |  | ||||||
|     if (this._listeners[storageKey]) { |  | ||||||
|       this._listeners[storageKey].push(callback); |  | ||||||
|     } else { |  | ||||||
|       this._listeners[storageKey] = [callback]; |  | ||||||
|     } |  | ||||||
|     return () => { |  | ||||||
|       this.unsubscribeChanges(storageKey, callback); |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public unsubscribeChanges(storageKey: string, callback: Callback) { |  | ||||||
|     if (!(storageKey in this._listeners)) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     const index = this._listeners[storageKey].indexOf(callback); |  | ||||||
|     if (index !== -1) { |  | ||||||
|       this._listeners[storageKey].splice(index, 1); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public hasKey(storageKey: string): any { |  | ||||||
|     return storageKey in this._storage; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public getValue(storageKey: string): any { |  | ||||||
|     return this._storage[storageKey]; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public setValue(storageKey: string, value: any): any { |  | ||||||
|     this._storage[storageKey] = value; |  | ||||||
|     try { |  | ||||||
|       window.localStorage.setItem(storageKey, JSON.stringify(value)); |  | ||||||
|     } catch (err) { |  | ||||||
|       // Safari in private mode doesn't allow localstorage |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const storage = new Storage(); |  | ||||||
|  |  | ||||||
| export const LocalStorage = ( |  | ||||||
|   storageKey?: string, |  | ||||||
|   property?: boolean, |  | ||||||
|   propertyOptions?: PropertyDeclaration |  | ||||||
| ): any => { |  | ||||||
|   return (clsElement: ClassElement) => { |  | ||||||
|     const key = String(clsElement.key); |  | ||||||
|     storageKey = storageKey || String(clsElement.key); |  | ||||||
|     const initVal = clsElement.initializer |  | ||||||
|       ? clsElement.initializer() |  | ||||||
|       : undefined; |  | ||||||
|  |  | ||||||
|     storage.addFromStorage(storageKey); |  | ||||||
|  |  | ||||||
|     const subscribe = (el: UpdatingElement): UnsubscribeFunc => |  | ||||||
|       storage.subscribeChanges(storageKey!, (oldValue) => { |  | ||||||
|         el.requestUpdate(clsElement.key, oldValue); |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|     const getValue = (): any => { |  | ||||||
|       return storage.hasKey(storageKey!) |  | ||||||
|         ? storage.getValue(storageKey!) |  | ||||||
|         : initVal; |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     const setValue = (el: UpdatingElement, value: any) => { |  | ||||||
|       let oldValue: unknown | undefined; |  | ||||||
|       if (property) { |  | ||||||
|         oldValue = getValue(); |  | ||||||
|       } |  | ||||||
|       storage.setValue(storageKey!, value); |  | ||||||
|       if (property) { |  | ||||||
|         el.requestUpdate(clsElement.key, oldValue); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|       kind: "method", |  | ||||||
|       placement: "prototype", |  | ||||||
|       key: clsElement.key, |  | ||||||
|       descriptor: { |  | ||||||
|         set(this: UpdatingElement, value: unknown) { |  | ||||||
|           setValue(this, value); |  | ||||||
|         }, |  | ||||||
|         get() { |  | ||||||
|           return getValue(); |  | ||||||
|         }, |  | ||||||
|         enumerable: true, |  | ||||||
|         configurable: true, |  | ||||||
|       }, |  | ||||||
|       finisher(cls: typeof UpdatingElement) { |  | ||||||
|         if (property) { |  | ||||||
|           const connectedCallback = cls.prototype.connectedCallback; |  | ||||||
|           const disconnectedCallback = cls.prototype.disconnectedCallback; |  | ||||||
|           cls.prototype.connectedCallback = function () { |  | ||||||
|             connectedCallback.call(this); |  | ||||||
|             this[`__unbsubLocalStorage${key}`] = subscribe(this); |  | ||||||
|           }; |  | ||||||
|           cls.prototype.disconnectedCallback = function () { |  | ||||||
|             disconnectedCallback.call(this); |  | ||||||
|             this[`__unbsubLocalStorage${key}`](); |  | ||||||
|           }; |  | ||||||
|           cls.createProperty(clsElement.key, { |  | ||||||
|             noAccessor: true, |  | ||||||
|             ...propertyOptions, |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|     }; |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
| @@ -22,6 +22,9 @@ const _load = ( | |||||||
|         (element as HTMLScriptElement).async = true; |         (element as HTMLScriptElement).async = true; | ||||||
|         if (type) { |         if (type) { | ||||||
|           (element as HTMLScriptElement).type = type; |           (element as HTMLScriptElement).type = type; | ||||||
|  |           // https://github.com/home-assistant/frontend/pull/6328 | ||||||
|  |           (element as HTMLScriptElement).crossOrigin = | ||||||
|  |             url.substr(0, 1) === "/" ? "use-credentials" : "anonymous"; | ||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
|       case "link": |       case "link": | ||||||
|   | |||||||
| @@ -3,51 +3,49 @@ import { HassEntity } from "home-assistant-js-websocket"; | |||||||
| /** Return an icon representing a binary sensor state. */ | /** Return an icon representing a binary sensor state. */ | ||||||
|  |  | ||||||
| export const binarySensorIcon = (state: HassEntity) => { | export const binarySensorIcon = (state: HassEntity) => { | ||||||
|   const is_off = state.state && state.state === "off"; |   const activated = state.state && state.state === "off"; | ||||||
|   switch (state.attributes.device_class) { |   switch (state.attributes.device_class) { | ||||||
|     case "battery": |     case "battery": | ||||||
|       return is_off ? "hass:battery" : "hass:battery-outline"; |       return activated ? "hass:battery" : "hass:battery-outline"; | ||||||
|     case "battery_charging": |  | ||||||
|       return is_off ? "hass:battery" : "hass:battery-charging"; |  | ||||||
|     case "cold": |     case "cold": | ||||||
|       return is_off ? "hass:thermometer" : "hass:snowflake"; |       return activated ? "hass:thermometer" : "hass:snowflake"; | ||||||
|     case "connectivity": |     case "connectivity": | ||||||
|       return is_off ? "hass:server-network-off" : "hass:server-network"; |       return activated ? "hass:server-network-off" : "hass:server-network"; | ||||||
|     case "door": |     case "door": | ||||||
|       return is_off ? "hass:door-closed" : "hass:door-open"; |       return activated ? "hass:door-closed" : "hass:door-open"; | ||||||
|     case "garage_door": |     case "garage_door": | ||||||
|       return is_off ? "hass:garage" : "hass:garage-open"; |       return activated ? "hass:garage" : "hass:garage-open"; | ||||||
|     case "gas": |     case "gas": | ||||||
|     case "power": |     case "power": | ||||||
|     case "problem": |     case "problem": | ||||||
|     case "safety": |     case "safety": | ||||||
|     case "smoke": |     case "smoke": | ||||||
|       return is_off ? "hass:shield-check" : "hass:alert"; |       return activated ? "hass:shield-check" : "hass:alert"; | ||||||
|     case "heat": |     case "heat": | ||||||
|       return is_off ? "hass:thermometer" : "hass:fire"; |       return activated ? "hass:thermometer" : "hass:fire"; | ||||||
|     case "light": |     case "light": | ||||||
|       return is_off ? "hass:brightness-5" : "hass:brightness-7"; |       return activated ? "hass:brightness-5" : "hass:brightness-7"; | ||||||
|     case "lock": |     case "lock": | ||||||
|       return is_off ? "hass:lock" : "hass:lock-open"; |       return activated ? "hass:lock" : "hass:lock-open"; | ||||||
|     case "moisture": |     case "moisture": | ||||||
|       return is_off ? "hass:water-off" : "hass:water"; |       return activated ? "hass:water-off" : "hass:water"; | ||||||
|     case "motion": |     case "motion": | ||||||
|       return is_off ? "hass:walk" : "hass:run"; |       return activated ? "hass:walk" : "hass:run"; | ||||||
|     case "occupancy": |     case "occupancy": | ||||||
|       return is_off ? "hass:home-outline" : "hass:home"; |       return activated ? "hass:home-outline" : "hass:home"; | ||||||
|     case "opening": |     case "opening": | ||||||
|       return is_off ? "hass:square" : "hass:square-outline"; |       return activated ? "hass:square" : "hass:square-outline"; | ||||||
|     case "plug": |     case "plug": | ||||||
|       return is_off ? "hass:power-plug-off" : "hass:power-plug"; |       return activated ? "hass:power-plug-off" : "hass:power-plug"; | ||||||
|     case "presence": |     case "presence": | ||||||
|       return is_off ? "hass:home-outline" : "hass:home"; |       return activated ? "hass:home-outline" : "hass:home"; | ||||||
|     case "sound": |     case "sound": | ||||||
|       return is_off ? "hass:music-note-off" : "hass:music-note"; |       return activated ? "hass:music-note-off" : "hass:music-note"; | ||||||
|     case "vibration": |     case "vibration": | ||||||
|       return is_off ? "hass:crop-portrait" : "hass:vibrate"; |       return activated ? "hass:crop-portrait" : "hass:vibrate"; | ||||||
|     case "window": |     case "window": | ||||||
|       return is_off ? "hass:window-closed" : "hass:window-open"; |       return activated ? "hass:window-closed" : "hass:window-open"; | ||||||
|     default: |     default: | ||||||
|       return is_off ? "hass:radiobox-blank" : "hass:checkbox-marked-circle"; |       return activated ? "hass:radiobox-blank" : "hass:checkbox-marked-circle"; | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -5,16 +5,12 @@ import { domainIcon } from "./domain_icon"; | |||||||
| import { batteryIcon } from "./battery_icon"; | import { batteryIcon } from "./battery_icon"; | ||||||
|  |  | ||||||
| const fixedDeviceClassIcons = { | const fixedDeviceClassIcons = { | ||||||
|   current: "hass:current-ac", |  | ||||||
|   energy: "hass:flash", |  | ||||||
|   humidity: "hass:water-percent", |   humidity: "hass:water-percent", | ||||||
|   illuminance: "hass:brightness-5", |   illuminance: "hass:brightness-5", | ||||||
|   temperature: "hass:thermometer", |   temperature: "hass:thermometer", | ||||||
|   pressure: "hass:gauge", |   pressure: "hass:gauge", | ||||||
|   power: "hass:flash", |   power: "hass:flash", | ||||||
|   power_factor: "hass:angle-acute", |  | ||||||
|   signal_strength: "hass:wifi", |   signal_strength: "hass:wifi", | ||||||
|   voltage: "hass:sine-wave", |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const sensorIcon = (state: HassEntity) => { | export const sensorIcon = (state: HassEntity) => { | ||||||
|   | |||||||
| @@ -1,12 +1,7 @@ | |||||||
| import { HassEntity } from "home-assistant-js-websocket"; | import { HassEntity } from "home-assistant-js-websocket"; | ||||||
| import durationToSeconds from "../datetime/duration_to_seconds"; | import durationToSeconds from "../datetime/duration_to_seconds"; | ||||||
|  |  | ||||||
| export const timerTimeRemaining = ( | export const timerTimeRemaining = (stateObj: HassEntity) => { | ||||||
|   stateObj: HassEntity |  | ||||||
| ): undefined | number => { |  | ||||||
|   if (!stateObj.attributes.remaining) { |  | ||||||
|     return undefined; |  | ||||||
|   } |  | ||||||
|   let timeRemaining = durationToSeconds(stateObj.attributes.remaining); |   let timeRemaining = durationToSeconds(stateObj.attributes.remaining); | ||||||
|  |  | ||||||
|   if (stateObj.state === "active") { |   if (stateObj.state === "active") { | ||||||
|   | |||||||
| @@ -1,14 +1,14 @@ | |||||||
| import { | import { | ||||||
|   ListItem, |  | ||||||
|   RequestSelectedDetail, |   RequestSelectedDetail, | ||||||
|  |   ListItem, | ||||||
| } from "@material/mwc-list/mwc-list-item"; | } from "@material/mwc-list/mwc-list-item"; | ||||||
|  |  | ||||||
| export const shouldHandleRequestSelectedEvent = ( | export const shouldHandleRequestSelectedEvent = ( | ||||||
|   ev: CustomEvent<RequestSelectedDetail> |   ev: CustomEvent<RequestSelectedDetail> | ||||||
| ): boolean => { | ): boolean => { | ||||||
|   if (!ev.detail.selected || ev.detail.source !== "property") { |   if (!ev.detail.selected && ev.detail.source !== "property") { | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|   (ev.currentTarget as ListItem).selected = false; |   (ev.target as ListItem).selected = false; | ||||||
|   return true; |   return true; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,53 +0,0 @@ | |||||||
| export const throttle = (callback: Function, wait: number) => { |  | ||||||
|   let isCalled = false; |  | ||||||
|  |  | ||||||
|   return function (...args: any) { |  | ||||||
|     if (!isCalled) { |  | ||||||
|       callback(...args); |  | ||||||
|       isCalled = true; |  | ||||||
|       setTimeout(() => { |  | ||||||
|         isCalled = false; |  | ||||||
|       }, wait); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const throttleAndQueue = ( |  | ||||||
|   callback: Function, |  | ||||||
|   wait: number, |  | ||||||
|   delay?: number |  | ||||||
| ) => { |  | ||||||
|   let isCalled = false; |  | ||||||
|   let timer: number | undefined; |  | ||||||
|   let delaying = false; |  | ||||||
|  |  | ||||||
|   const processQueue = () => { |  | ||||||
|     if (isCalled) { |  | ||||||
|       callback(); |  | ||||||
|     } |  | ||||||
|     clearInterval(timer); |  | ||||||
|     timer = undefined; |  | ||||||
|     isCalled = false; |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   const setUpThrottle = () => { |  | ||||||
|     delaying = false; |  | ||||||
|  |  | ||||||
|     processQueue(); // start immediately on the first invocation |  | ||||||
|     timer = window.setInterval(processQueue, wait); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return () => { |  | ||||||
|     isCalled = true; |  | ||||||
|     if (timer !== undefined) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (delay === undefined) { |  | ||||||
|       setUpThrottle(); |  | ||||||
|     } else if (!delaying) { |  | ||||||
|       delaying = true; |  | ||||||
|       setTimeout(setUpThrottle, delay); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
							
								
								
									
										110
									
								
								src/components/buttons/ha-progress-button.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/components/buttons/ha-progress-button.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | |||||||
|  | import "@material/mwc-button"; | ||||||
|  | import "../ha-circular-progress"; | ||||||
|  | import { html } from "@polymer/polymer/lib/utils/html-tag"; | ||||||
|  | /* eslint-plugin-disable lit */ | ||||||
|  | import { PolymerElement } from "@polymer/polymer/polymer-element"; | ||||||
|  |  | ||||||
|  | class HaProgressButton extends PolymerElement { | ||||||
|  |   static get template() { | ||||||
|  |     return html` | ||||||
|  |       <style> | ||||||
|  |         :host { | ||||||
|  |           outline: none; | ||||||
|  |         } | ||||||
|  |         .container { | ||||||
|  |           position: relative; | ||||||
|  |           display: inline-block; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         mwc-button { | ||||||
|  |           transition: all 1s; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .success mwc-button { | ||||||
|  |           --mdc-theme-primary: white; | ||||||
|  |           background-color: var(--success-color); | ||||||
|  |           transition: none; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .error mwc-button { | ||||||
|  |           --mdc-theme-primary: white; | ||||||
|  |           background-color: var(--error-color); | ||||||
|  |           transition: none; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .progress { | ||||||
|  |           @apply --layout; | ||||||
|  |           @apply --layout-center-center; | ||||||
|  |           position: absolute; | ||||||
|  |           top: 0; | ||||||
|  |           left: 0; | ||||||
|  |           right: 0; | ||||||
|  |           bottom: 0; | ||||||
|  |         } | ||||||
|  |       </style> | ||||||
|  |       <div class="container" id="container"> | ||||||
|  |         <mwc-button | ||||||
|  |           id="button" | ||||||
|  |           disabled="[[computeDisabled(disabled, progress)]]" | ||||||
|  |           on-click="buttonTapped" | ||||||
|  |         > | ||||||
|  |           <slot></slot> | ||||||
|  |         </mwc-button> | ||||||
|  |         <template is="dom-if" if="[[progress]]"> | ||||||
|  |           <div class="progress"> | ||||||
|  |             <ha-circular-progress active size="small"></ha-circular-progress> | ||||||
|  |           </div> | ||||||
|  |         </template> | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static get properties() { | ||||||
|  |     return { | ||||||
|  |       hass: { | ||||||
|  |         type: Object, | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       progress: { | ||||||
|  |         type: Boolean, | ||||||
|  |         value: false, | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       disabled: { | ||||||
|  |         type: Boolean, | ||||||
|  |         value: false, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   tempClass(className) { | ||||||
|  |     const classList = this.$.container.classList; | ||||||
|  |     classList.add(className); | ||||||
|  |     setTimeout(() => { | ||||||
|  |       classList.remove(className); | ||||||
|  |     }, 1000); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ready() { | ||||||
|  |     super.ready(); | ||||||
|  |     this.addEventListener("click", (ev) => this.buttonTapped(ev)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   buttonTapped(ev) { | ||||||
|  |     if (this.progress) ev.stopPropagation(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   actionSuccess() { | ||||||
|  |     this.tempClass("success"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   actionError() { | ||||||
|  |     this.tempClass("error"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   computeDisabled(disabled, progress) { | ||||||
|  |     return disabled || progress; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | customElements.define("ha-progress-button", HaProgressButton); | ||||||
| @@ -1,114 +0,0 @@ | |||||||
| import "@material/mwc-button"; |  | ||||||
| import type { Button } from "@material/mwc-button"; |  | ||||||
| import { |  | ||||||
|   css, |  | ||||||
|   CSSResult, |  | ||||||
|   customElement, |  | ||||||
|   html, |  | ||||||
|   LitElement, |  | ||||||
|   property, |  | ||||||
|   TemplateResult, |  | ||||||
|   query, |  | ||||||
| } from "lit-element"; |  | ||||||
|  |  | ||||||
| import "../ha-circular-progress"; |  | ||||||
|  |  | ||||||
| @customElement("ha-progress-button") |  | ||||||
| class HaProgressButton extends LitElement { |  | ||||||
|   @property({ type: Boolean }) public disabled = false; |  | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public progress = false; |  | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public raised = false; |  | ||||||
|  |  | ||||||
|   @query("mwc-button") private _button?: Button; |  | ||||||
|  |  | ||||||
|   public render(): TemplateResult { |  | ||||||
|     return html` |  | ||||||
|       <mwc-button |  | ||||||
|         ?raised=${this.raised} |  | ||||||
|         .disabled=${this.disabled || this.progress} |  | ||||||
|         @click=${this._buttonTapped} |  | ||||||
|       > |  | ||||||
|         <slot></slot> |  | ||||||
|       </mwc-button> |  | ||||||
|       ${this.progress |  | ||||||
|         ? html`<div class="progress"> |  | ||||||
|             <ha-circular-progress size="small" active></ha-circular-progress> |  | ||||||
|           </div>` |  | ||||||
|         : ""} |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public actionSuccess(): void { |  | ||||||
|     this._tempClass("success"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public actionError(): void { |  | ||||||
|     this._tempClass("error"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _tempClass(className: string): void { |  | ||||||
|     this._button!.classList.add(className); |  | ||||||
|     setTimeout(() => { |  | ||||||
|       this._button!.classList.remove(className); |  | ||||||
|     }, 1000); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _buttonTapped(ev: Event): void { |  | ||||||
|     if (this.progress) { |  | ||||||
|       ev.stopPropagation(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult { |  | ||||||
|     return css` |  | ||||||
|       :host { |  | ||||||
|         outline: none; |  | ||||||
|         display: inline-block; |  | ||||||
|         position: relative; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       mwc-button { |  | ||||||
|         transition: all 1s; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       mwc-button.success { |  | ||||||
|         --mdc-theme-primary: white; |  | ||||||
|         background-color: var(--success-color); |  | ||||||
|         transition: none; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       mwc-button[raised].success { |  | ||||||
|         --mdc-theme-primary: var(--success-color); |  | ||||||
|         --mdc-theme-on-primary: white; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       mwc-button.error { |  | ||||||
|         --mdc-theme-primary: white; |  | ||||||
|         background-color: var(--error-color); |  | ||||||
|         transition: none; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       mwc-button[raised].error { |  | ||||||
|         --mdc-theme-primary: var(--error-color); |  | ||||||
|         --mdc-theme-on-primary: white; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .progress { |  | ||||||
|         bottom: 0; |  | ||||||
|         margin-top: 4px; |  | ||||||
|         position: absolute; |  | ||||||
|         text-align: center; |  | ||||||
|         top: 0; |  | ||||||
|         width: 100%; |  | ||||||
|       } |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "ha-progress-button": HaProgressButton; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -3,21 +3,19 @@ import { | |||||||
|   css, |   css, | ||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   eventOptions, |  | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   PropertyValues, |   PropertyValues, | ||||||
|   query, |   query, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
|  |   eventOptions, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { classMap } from "lit-html/directives/class-map"; | import { classMap } from "lit-html/directives/class-map"; | ||||||
| import { ifDefined } from "lit-html/directives/if-defined"; | import { ifDefined } from "lit-html/directives/if-defined"; | ||||||
| import { styleMap } from "lit-html/directives/style-map"; | import { styleMap } from "lit-html/directives/style-map"; | ||||||
| import { scroll } from "lit-virtualizer"; | import { scroll } from "lit-virtualizer"; | ||||||
| import memoizeOne from "memoize-one"; |  | ||||||
| import { restoreScroll } from "../../common/decorators/restore-scroll"; |  | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import "../../common/search/search-input"; | import "../../common/search/search-input"; | ||||||
| import { debounce } from "../../common/util/debounce"; | import { debounce } from "../../common/util/debounce"; | ||||||
| @@ -26,6 +24,8 @@ import "../ha-checkbox"; | |||||||
| import type { HaCheckbox } from "../ha-checkbox"; | import type { HaCheckbox } from "../ha-checkbox"; | ||||||
| import "../ha-icon"; | import "../ha-icon"; | ||||||
| import { filterData, sortData } from "./sort-filter"; | import { filterData, sortData } from "./sort-filter"; | ||||||
|  | import memoizeOne from "memoize-one"; | ||||||
|  | import { restoreScroll } from "../../common/decorators/restore-scroll"; | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   // for fire event |   // for fire event | ||||||
| @@ -70,7 +70,6 @@ export interface DataTableColumnData extends DataTableSortColumnData { | |||||||
|   maxWidth?: string; |   maxWidth?: string; | ||||||
|   grows?: boolean; |   grows?: boolean; | ||||||
|   forceLTR?: boolean; |   forceLTR?: boolean; | ||||||
|   hidden?: boolean; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface DataTableRowData { | export interface DataTableRowData { | ||||||
| @@ -215,15 +214,13 @@ export class HaDataTable extends LitElement { | |||||||
|           class="mdc-data-table__table ${classMap({ |           class="mdc-data-table__table ${classMap({ | ||||||
|             "auto-height": this.autoHeight, |             "auto-height": this.autoHeight, | ||||||
|           })}" |           })}" | ||||||
|           role="table" |  | ||||||
|           aria-rowcount=${this._filteredData.length} |  | ||||||
|           style=${styleMap({ |           style=${styleMap({ | ||||||
|             height: this.autoHeight |             height: this.autoHeight | ||||||
|               ? `${(this._filteredData.length || 1) * 53 + 57}px` |               ? `${(this._filteredData.length || 1) * 53 + 57}px` | ||||||
|               : `calc(100% - ${this._header?.clientHeight}px)`, |               : `calc(100% - ${this._header?.clientHeight}px)`, | ||||||
|           })} |           })} | ||||||
|         > |         > | ||||||
|           <div class="mdc-data-table__header-row" role="row"> |           <div class="mdc-data-table__header-row"> | ||||||
|             ${this.selectable |             ${this.selectable | ||||||
|               ? html` |               ? html` | ||||||
|                   <div |                   <div | ||||||
| @@ -243,10 +240,8 @@ export class HaDataTable extends LitElement { | |||||||
|                   </div> |                   </div> | ||||||
|                 ` |                 ` | ||||||
|               : ""} |               : ""} | ||||||
|             ${Object.entries(this.columns).map(([key, column]) => { |             ${Object.entries(this.columns).map((columnEntry) => { | ||||||
|               if (column.hidden) { |               const [key, column] = columnEntry; | ||||||
|                 return ""; |  | ||||||
|               } |  | ||||||
|               const sorted = key === this._sortColumn; |               const sorted = key === this._sortColumn; | ||||||
|               const classes = { |               const classes = { | ||||||
|                 "mdc-data-table__header-cell--numeric": Boolean( |                 "mdc-data-table__header-cell--numeric": Boolean( | ||||||
| @@ -293,8 +288,8 @@ export class HaDataTable extends LitElement { | |||||||
|           ${!this._filteredData.length |           ${!this._filteredData.length | ||||||
|             ? html` |             ? html` | ||||||
|                 <div class="mdc-data-table__content"> |                 <div class="mdc-data-table__content"> | ||||||
|                   <div class="mdc-data-table__row" role="row"> |                   <div class="mdc-data-table__row"> | ||||||
|                     <div class="mdc-data-table__cell grows center" role="cell"> |                     <div class="mdc-data-table__cell grows center"> | ||||||
|                       ${this.noDataText || "No data"} |                       ${this.noDataText || "No data"} | ||||||
|                     </div> |                     </div> | ||||||
|                   </div> |                   </div> | ||||||
| @@ -309,14 +304,12 @@ export class HaDataTable extends LitElement { | |||||||
|                     items: !this.hasFab |                     items: !this.hasFab | ||||||
|                       ? this._filteredData |                       ? this._filteredData | ||||||
|                       : [...this._filteredData, ...[{ empty: true }]], |                       : [...this._filteredData, ...[{ empty: true }]], | ||||||
|                     renderItem: (row: DataTableRowData, index) => { |                     renderItem: (row: DataTableRowData) => { | ||||||
|                       if (row.empty) { |                       if (row.empty) { | ||||||
|                         return html` <div class="mdc-data-table__row"></div> `; |                         return html` <div class="mdc-data-table__row"></div> `; | ||||||
|                       } |                       } | ||||||
|                       return html` |                       return html` | ||||||
|                         <div |                         <div | ||||||
|                           aria-rowindex=${index} |  | ||||||
|                           role="row" |  | ||||||
|                           .rowId="${row[this.id]}" |                           .rowId="${row[this.id]}" | ||||||
|                           @click=${this._handleRowClick} |                           @click=${this._handleRowClick} | ||||||
|                           class="mdc-data-table__row ${classMap({ |                           class="mdc-data-table__row ${classMap({ | ||||||
| @@ -335,7 +328,6 @@ export class HaDataTable extends LitElement { | |||||||
|                             ? html` |                             ? html` | ||||||
|                                 <div |                                 <div | ||||||
|                                   class="mdc-data-table__cell mdc-data-table__cell--checkbox" |                                   class="mdc-data-table__cell mdc-data-table__cell--checkbox" | ||||||
|                                   role="cell" |  | ||||||
|                                 > |                                 > | ||||||
|                                   <ha-checkbox |                                   <ha-checkbox | ||||||
|                                     class="mdc-data-table__row-checkbox" |                                     class="mdc-data-table__row-checkbox" | ||||||
| @@ -349,14 +341,10 @@ export class HaDataTable extends LitElement { | |||||||
|                                 </div> |                                 </div> | ||||||
|                               ` |                               ` | ||||||
|                             : ""} |                             : ""} | ||||||
|                           ${Object.entries(this.columns).map( |                           ${Object.entries(this.columns).map((columnEntry) => { | ||||||
|                             ([key, column]) => { |                             const [key, column] = columnEntry; | ||||||
|                               if (column.hidden) { |  | ||||||
|                                 return ""; |  | ||||||
|                               } |  | ||||||
|                             return html` |                             return html` | ||||||
|                               <div |                               <div | ||||||
|                                   role="cell" |  | ||||||
|                                 class="mdc-data-table__cell ${classMap({ |                                 class="mdc-data-table__cell ${classMap({ | ||||||
|                                   "mdc-data-table__cell--numeric": Boolean( |                                   "mdc-data-table__cell--numeric": Boolean( | ||||||
|                                     column.type === "numeric" |                                     column.type === "numeric" | ||||||
| @@ -386,8 +374,7 @@ export class HaDataTable extends LitElement { | |||||||
|                                   : row[key]} |                                   : row[key]} | ||||||
|                               </div> |                               </div> | ||||||
|                             `; |                             `; | ||||||
|                             } |                           })} | ||||||
|                           )} |  | ||||||
|                         </div> |                         </div> | ||||||
|                       `; |                       `; | ||||||
|                     }, |                     }, | ||||||
|   | |||||||
| @@ -1,11 +1,11 @@ | |||||||
| // To use comlink under ES5 | // To use comlink under ES5 | ||||||
| import { expose } from "comlink"; |  | ||||||
| import "proxy-polyfill"; | import "proxy-polyfill"; | ||||||
|  | import { expose } from "comlink"; | ||||||
| import type { | import type { | ||||||
|   DataTableRowData, |  | ||||||
|   DataTableSortColumnData, |   DataTableSortColumnData, | ||||||
|   SortableColumnContainer, |   DataTableRowData, | ||||||
|   SortingDirection, |   SortingDirection, | ||||||
|  |   SortableColumnContainer, | ||||||
| } from "./ha-data-table"; | } from "./ha-data-table"; | ||||||
|  |  | ||||||
| const filterData = ( | const filterData = ( | ||||||
| @@ -19,7 +19,7 @@ const filterData = ( | |||||||
|       const [key, column] = columnEntry; |       const [key, column] = columnEntry; | ||||||
|       if (column.filterable) { |       if (column.filterable) { | ||||||
|         if ( |         if ( | ||||||
|           String(column.filterKey ? row[key][column.filterKey] : row[key]) |           (column.filterKey ? row[key][column.filterKey] : row[key]) | ||||||
|             .toUpperCase() |             .toUpperCase() | ||||||
|             .includes(filter) |             .includes(filter) | ||||||
|         ) { |         ) { | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| /* eslint-plugin-disable lit */ | /* eslint-plugin-disable lit */ | ||||||
| import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior"; | import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior"; | ||||||
|  | import "../ha-icon-button"; | ||||||
| import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class"; | import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class"; | ||||||
| import { timeOut } from "@polymer/polymer/lib/utils/async"; | import { timeOut } from "@polymer/polymer/lib/utils/async"; | ||||||
| import { Debouncer } from "@polymer/polymer/lib/utils/debounce"; | import { Debouncer } from "@polymer/polymer/lib/utils/debounce"; | ||||||
| import { html } from "@polymer/polymer/lib/utils/html-tag"; | import { html } from "@polymer/polymer/lib/utils/html-tag"; | ||||||
| import { PolymerElement } from "@polymer/polymer/polymer-element"; | import { PolymerElement } from "@polymer/polymer/polymer-element"; | ||||||
| import { formatTime } from "../../common/datetime/format_time"; | import { formatTime } from "../../common/datetime/format_time"; | ||||||
| import "../ha-icon-button"; |  | ||||||
|  |  | ||||||
| // eslint-disable-next-line no-unused-vars | // eslint-disable-next-line no-unused-vars | ||||||
| /* global Chart moment Color */ | /* global Chart moment Color */ | ||||||
| @@ -355,7 +355,7 @@ class HaChartBase extends mixinBehaviors( | |||||||
|       return value; |       return value; | ||||||
|     } |     } | ||||||
|     const date = new Date(values[index].value); |     const date = new Date(values[index].value); | ||||||
|     return formatTime(date, this.hass.language); |     return formatTime(date); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   drawChart() { |   drawChart() { | ||||||
| @@ -420,7 +420,7 @@ class HaChartBase extends mixinBehaviors( | |||||||
|         }, |         }, | ||||||
|       }; |       }; | ||||||
|       options = Chart.helpers.merge(options, this.data.options); |       options = Chart.helpers.merge(options, this.data.options); | ||||||
|       options.scales.xAxes[0].ticks.callback = this._formatTickValue.bind(this); |       options.scales.xAxes[0].ticks.callback = this._formatTickValue; | ||||||
|       if (this.data.type === "timeline") { |       if (this.data.type === "timeline") { | ||||||
|         this.set("isTimeline", true); |         this.set("isTimeline", true); | ||||||
|         if (this.data.colors !== undefined) { |         if (this.data.colors !== undefined) { | ||||||
|   | |||||||
| @@ -1,178 +0,0 @@ | |||||||
| import "@polymer/paper-input/paper-input"; |  | ||||||
| import "@polymer/paper-item/paper-item"; |  | ||||||
| import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; |  | ||||||
| import { HassEntity } from "home-assistant-js-websocket"; |  | ||||||
| import { |  | ||||||
|   css, |  | ||||||
|   CSSResult, |  | ||||||
|   customElement, |  | ||||||
|   html, |  | ||||||
|   LitElement, |  | ||||||
|   property, |  | ||||||
|   PropertyValues, |  | ||||||
|   query, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit-element"; |  | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; |  | ||||||
| import { PolymerChangedEvent } from "../../polymer-types"; |  | ||||||
| import { HomeAssistant } from "../../types"; |  | ||||||
| import "../ha-icon-button"; |  | ||||||
| import "./state-badge"; |  | ||||||
|  |  | ||||||
| export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; |  | ||||||
|  |  | ||||||
| const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => { |  | ||||||
|   if (!root.firstElementChild) { |  | ||||||
|     root.innerHTML = ` |  | ||||||
|       <style> |  | ||||||
|         paper-item { |  | ||||||
|           margin: -10px; |  | ||||||
|           padding: 0; |  | ||||||
|         } |  | ||||||
|       </style> |  | ||||||
|       <paper-item></paper-item> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|   root.querySelector("paper-item")!.textContent = model.item; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| @customElement("ha-entity-attribute-picker") |  | ||||||
| class HaEntityAttributePicker extends LitElement { |  | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   @property() public entityId?: string; |  | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public autofocus = false; |  | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public disabled = false; |  | ||||||
|  |  | ||||||
|   @property({ type: Boolean, attribute: "allow-custom-value" }) |  | ||||||
|   public allowCustomValue; |  | ||||||
|  |  | ||||||
|   @property() public label?: string; |  | ||||||
|  |  | ||||||
|   @property() public value?: string; |  | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) private _opened = false; |  | ||||||
|  |  | ||||||
|   @query("vaadin-combo-box-light") private _comboBox!: HTMLElement; |  | ||||||
|  |  | ||||||
|   protected shouldUpdate(changedProps: PropertyValues) { |  | ||||||
|     return !(!changedProps.has("_opened") && this._opened); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected updated(changedProps: PropertyValues) { |  | ||||||
|     if (changedProps.has("_opened") && this._opened) { |  | ||||||
|       const state = this.entityId ? this.hass.states[this.entityId] : undefined; |  | ||||||
|       (this._comboBox as any).items = state |  | ||||||
|         ? Object.keys(state.attributes) |  | ||||||
|         : []; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |  | ||||||
|     if (!this.hass) { |  | ||||||
|       return html``; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return html` |  | ||||||
|       <vaadin-combo-box-light |  | ||||||
|         .value=${this._value} |  | ||||||
|         .allowCustomValue=${this.allowCustomValue} |  | ||||||
|         .renderer=${rowRenderer} |  | ||||||
|         @opened-changed=${this._openedChanged} |  | ||||||
|         @value-changed=${this._valueChanged} |  | ||||||
|       > |  | ||||||
|         <paper-input |  | ||||||
|           .autofocus=${this.autofocus} |  | ||||||
|           .label=${this.label ?? |  | ||||||
|           this.hass.localize( |  | ||||||
|             "ui.components.entity.entity-attribute-picker.attribute" |  | ||||||
|           )} |  | ||||||
|           .value=${this._value} |  | ||||||
|           .disabled=${this.disabled || !this.entityId} |  | ||||||
|           class="input" |  | ||||||
|           autocapitalize="none" |  | ||||||
|           autocomplete="off" |  | ||||||
|           autocorrect="off" |  | ||||||
|           spellcheck="false" |  | ||||||
|         > |  | ||||||
|           ${this.value |  | ||||||
|             ? html` |  | ||||||
|                 <ha-icon-button |  | ||||||
|                   aria-label=${this.hass.localize( |  | ||||||
|                     "ui.components.entity.entity-picker.clear" |  | ||||||
|                   )} |  | ||||||
|                   slot="suffix" |  | ||||||
|                   class="clear-button" |  | ||||||
|                   icon="hass:close" |  | ||||||
|                   @click=${this._clearValue} |  | ||||||
|                   no-ripple |  | ||||||
|                 > |  | ||||||
|                   Clear |  | ||||||
|                 </ha-icon-button> |  | ||||||
|               ` |  | ||||||
|             : ""} |  | ||||||
|  |  | ||||||
|           <ha-icon-button |  | ||||||
|             aria-label=${this.hass.localize( |  | ||||||
|               "ui.components.entity.entity-attribute-picker.show_attributes" |  | ||||||
|             )} |  | ||||||
|             slot="suffix" |  | ||||||
|             class="toggle-button" |  | ||||||
|             .icon=${this._opened ? "hass:menu-up" : "hass:menu-down"} |  | ||||||
|           > |  | ||||||
|             Toggle |  | ||||||
|           </ha-icon-button> |  | ||||||
|         </paper-input> |  | ||||||
|       </vaadin-combo-box-light> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _clearValue(ev: Event) { |  | ||||||
|     ev.stopPropagation(); |  | ||||||
|     this._setValue(""); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private get _value() { |  | ||||||
|     return this.value || ""; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _openedChanged(ev: PolymerChangedEvent<boolean>) { |  | ||||||
|     this._opened = ev.detail.value; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _valueChanged(ev: PolymerChangedEvent<string>) { |  | ||||||
|     const newValue = ev.detail.value; |  | ||||||
|     if (newValue !== this._value) { |  | ||||||
|       this._setValue(newValue); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _setValue(value: string) { |  | ||||||
|     this.value = value; |  | ||||||
|     setTimeout(() => { |  | ||||||
|       fireEvent(this, "value-changed", { value }); |  | ||||||
|       fireEvent(this, "change"); |  | ||||||
|     }, 0); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult { |  | ||||||
|     return css` |  | ||||||
|       paper-input > ha-icon-button { |  | ||||||
|         --mdc-icon-button-size: 24px; |  | ||||||
|         padding: 0px 2px; |  | ||||||
|         color: var(--secondary-text-color); |  | ||||||
|       } |  | ||||||
|       [hidden] { |  | ||||||
|         display: none; |  | ||||||
|       } |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "ha-entity-attribute-picker": HaEntityAttributePicker; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import "../ha-icon-button"; | ||||||
| import "@polymer/paper-input/paper-input"; | import "@polymer/paper-input/paper-input"; | ||||||
| import "@polymer/paper-item/paper-icon-item"; | import "@polymer/paper-item/paper-icon-item"; | ||||||
| import "@polymer/paper-item/paper-item-body"; | import "@polymer/paper-item/paper-item-body"; | ||||||
| @@ -19,7 +20,6 @@ import { computeDomain } from "../../common/entity/compute_domain"; | |||||||
| import { computeStateName } from "../../common/entity/compute_state_name"; | import { computeStateName } from "../../common/entity/compute_state_name"; | ||||||
| import { PolymerChangedEvent } from "../../polymer-types"; | import { PolymerChangedEvent } from "../../polymer-types"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../types"; | ||||||
| import "../ha-icon-button"; |  | ||||||
| import "./state-badge"; | import "./state-badge"; | ||||||
|  |  | ||||||
| export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; | export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; | ||||||
| @@ -95,8 +95,6 @@ class HaEntityPicker extends LitElement { | |||||||
|  |  | ||||||
|   @query("vaadin-combo-box-light") private _comboBox!: HTMLElement; |   @query("vaadin-combo-box-light") private _comboBox!: HTMLElement; | ||||||
|  |  | ||||||
|   private _initedStates = false; |  | ||||||
|  |  | ||||||
|   private _getStates = memoizeOne( |   private _getStates = memoizeOne( | ||||||
|     ( |     ( | ||||||
|       _opened: boolean, |       _opened: boolean, | ||||||
| @@ -150,18 +148,11 @@ class HaEntityPicker extends LitElement { | |||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   protected shouldUpdate(changedProps: PropertyValues) { |   protected shouldUpdate(changedProps: PropertyValues) { | ||||||
|     if ( |  | ||||||
|       changedProps.has("value") || |  | ||||||
|       changedProps.has("label") || |  | ||||||
|       changedProps.has("disabled") |  | ||||||
|     ) { |  | ||||||
|       return true; |  | ||||||
|     } |  | ||||||
|     return !(!changedProps.has("_opened") && this._opened); |     return !(!changedProps.has("_opened") && this._opened); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected updated(changedProps: PropertyValues) { |   protected updated(changedProps: PropertyValues) { | ||||||
|     if (!this._initedStates || (changedProps.has("_opened") && this._opened)) { |     if (changedProps.has("_opened") && this._opened) { | ||||||
|       const states = this._getStates( |       const states = this._getStates( | ||||||
|         this._opened, |         this._opened, | ||||||
|         this.hass, |         this.hass, | ||||||
| @@ -171,7 +162,6 @@ class HaEntityPicker extends LitElement { | |||||||
|         this.includeDeviceClasses |         this.includeDeviceClasses | ||||||
|       ); |       ); | ||||||
|       (this._comboBox as any).items = states; |       (this._comboBox as any).items = states; | ||||||
|       this._initedStates = true; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -179,6 +169,7 @@ class HaEntityPicker extends LitElement { | |||||||
|     if (!this.hass) { |     if (!this.hass) { | ||||||
|       return html``; |       return html``; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <vaadin-combo-box-light |       <vaadin-combo-box-light | ||||||
|         item-value-path="entity_id" |         item-value-path="entity_id" | ||||||
|   | |||||||
| @@ -1,67 +0,0 @@ | |||||||
| import { |  | ||||||
|   css, |  | ||||||
|   CSSResult, |  | ||||||
|   customElement, |  | ||||||
|   LitElement, |  | ||||||
|   property, |  | ||||||
|   svg, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit-element"; |  | ||||||
|  |  | ||||||
| import { |  | ||||||
|   getValueInPercentage, |  | ||||||
|   normalize, |  | ||||||
|   roundWithOneDecimal, |  | ||||||
| } from "../util/calculate"; |  | ||||||
|  |  | ||||||
| @customElement("ha-bar") |  | ||||||
| export class HaBar extends LitElement { |  | ||||||
|   @property({ type: Number }) public min = 0; |  | ||||||
|  |  | ||||||
|   @property({ type: Number }) public max = 100; |  | ||||||
|  |  | ||||||
|   @property({ type: Number }) public value!: number; |  | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |  | ||||||
|     const valuePrecentage = roundWithOneDecimal( |  | ||||||
|       getValueInPercentage( |  | ||||||
|         normalize(this.value, this.min, this.max), |  | ||||||
|         this.min, |  | ||||||
|         this.max |  | ||||||
|       ) |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     return svg` |  | ||||||
|       <svg> |  | ||||||
|         <g> |  | ||||||
|           <rect></rect> |  | ||||||
|           <rect width="${valuePrecentage}%"></rect> |  | ||||||
|         </g> |  | ||||||
|       </svg> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult { |  | ||||||
|     return css` |  | ||||||
|       rect:first-child { |  | ||||||
|         width: 100%; |  | ||||||
|         fill: var(--ha-bar-background-color, var(--secondary-background-color)); |  | ||||||
|       } |  | ||||||
|       rect:last-child { |  | ||||||
|         fill: var(--ha-bar-primary-color, var(--primary-color)); |  | ||||||
|         rx: var(--ha-bar-border-radius, 4px); |  | ||||||
|       } |  | ||||||
|       svg { |  | ||||||
|         border-radius: var(--ha-bar-border-radius, 4px); |  | ||||||
|         height: 12px; |  | ||||||
|         width: 100%; |  | ||||||
|       } |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "ha-bar": HaBar; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,20 +1,21 @@ | |||||||
| import "@material/mwc-icon-button/mwc-icon-button"; |  | ||||||
| import { | import { | ||||||
|   css, |  | ||||||
|   CSSResult, |  | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   LitElement, |  | ||||||
|   property, |  | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
|  |   property, | ||||||
|  |   LitElement, | ||||||
|  |   CSSResult, | ||||||
|  |   css, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
|  |  | ||||||
|  | import "./ha-icon-button"; | ||||||
|  |  | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
| import type { ToggleButton } from "../types"; | import type { ToggleButton } from "../types"; | ||||||
| import "./ha-svg-icon"; |  | ||||||
|  |  | ||||||
| @customElement("ha-button-toggle-group") | @customElement("ha-button-toggle-group") | ||||||
| export class HaButtonToggleGroup extends LitElement { | export class HaButtonToggleGroup extends LitElement { | ||||||
|   @property({ attribute: false }) public buttons!: ToggleButton[]; |   @property() public buttons!: ToggleButton[]; | ||||||
|  |  | ||||||
|   @property() public active?: string; |   @property() public active?: string; | ||||||
|  |  | ||||||
| @@ -22,23 +23,21 @@ export class HaButtonToggleGroup extends LitElement { | |||||||
|     return html` |     return html` | ||||||
|       <div> |       <div> | ||||||
|         ${this.buttons.map( |         ${this.buttons.map( | ||||||
|           (button) => html` |           (button) => html` <ha-icon-button | ||||||
|             <mwc-icon-button |  | ||||||
|             .label=${button.label} |             .label=${button.label} | ||||||
|  |             .icon=${button.icon} | ||||||
|             .value=${button.value} |             .value=${button.value} | ||||||
|             ?active=${this.active === button.value} |             ?active=${this.active === button.value} | ||||||
|             @click=${this._handleClick} |             @click=${this._handleClick} | ||||||
|           > |           > | ||||||
|               <ha-svg-icon .path=${button.iconPath}></ha-svg-icon> |           </ha-icon-button>` | ||||||
|             </mwc-icon-button> |  | ||||||
|           ` |  | ||||||
|         )} |         )} | ||||||
|       </div> |       </div> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _handleClick(ev): void { |   private _handleClick(ev): void { | ||||||
|     this.active = ev.currentTarget.value; |     this.active = ev.target.value; | ||||||
|     fireEvent(this, "value-changed", { value: this.active }); |     fireEvent(this, "value-changed", { value: this.active }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -49,13 +48,12 @@ export class HaButtonToggleGroup extends LitElement { | |||||||
|         --mdc-icon-button-size: var(--button-toggle-size, 36px); |         --mdc-icon-button-size: var(--button-toggle-size, 36px); | ||||||
|         --mdc-icon-size: var(--button-toggle-icon-size, 20px); |         --mdc-icon-size: var(--button-toggle-icon-size, 20px); | ||||||
|       } |       } | ||||||
|       mwc-icon-button { |       ha-icon-button { | ||||||
|         border: 1px solid var(--primary-color); |         border: 1px solid var(--primary-color); | ||||||
|         border-right-width: 0px; |         border-right-width: 0px; | ||||||
|         position: relative; |         position: relative; | ||||||
|         cursor: pointer; |  | ||||||
|       } |       } | ||||||
|       mwc-icon-button::before { |       ha-icon-button::before { | ||||||
|         top: 0; |         top: 0; | ||||||
|         left: 0; |         left: 0; | ||||||
|         width: 100%; |         width: 100%; | ||||||
| @@ -67,26 +65,22 @@ export class HaButtonToggleGroup extends LitElement { | |||||||
|         content: ""; |         content: ""; | ||||||
|         transition: opacity 15ms linear, background-color 15ms linear; |         transition: opacity 15ms linear, background-color 15ms linear; | ||||||
|       } |       } | ||||||
|       mwc-icon-button[active]::before { |       ha-icon-button[active]::before { | ||||||
|         opacity: var(--mdc-icon-button-ripple-opacity, 0.12); |         opacity: var(--mdc-icon-button-ripple-opacity, 0.12); | ||||||
|       } |       } | ||||||
|       mwc-icon-button:first-child { |       ha-icon-button:first-child { | ||||||
|         border-radius: 4px 0 0 4px; |         border-radius: 4px 0 0 4px; | ||||||
|       } |       } | ||||||
|       mwc-icon-button:last-child { |       ha-icon-button:last-child { | ||||||
|         border-radius: 0 4px 4px 0; |         border-radius: 0 4px 4px 0; | ||||||
|         border-right-width: 1px; |         border-right-width: 1px; | ||||||
|       } |       } | ||||||
|       mwc-icon-button:only-child { |  | ||||||
|         border-radius: 4px; |  | ||||||
|         border-right-width: 1px; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
|     "ha-button-toggle-group": HaButtonToggleGroup; |     "ha-button-toggle-button": HaButtonToggleGroup; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,9 +3,9 @@ import { | |||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   PropertyValues, |   PropertyValues, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| @@ -18,24 +18,37 @@ import { | |||||||
|   fetchStreamUrl, |   fetchStreamUrl, | ||||||
| } from "../data/camera"; | } from "../data/camera"; | ||||||
| import { CameraEntity, HomeAssistant } from "../types"; | import { CameraEntity, HomeAssistant } from "../types"; | ||||||
| import "./ha-hls-player"; |  | ||||||
|  | type HLSModule = typeof import("hls.js"); | ||||||
|  |  | ||||||
| @customElement("ha-camera-stream") | @customElement("ha-camera-stream") | ||||||
| class HaCameraStream extends LitElement { | class HaCameraStream extends LitElement { | ||||||
|   @property({ attribute: false }) public hass?: HomeAssistant; |   @property({ attribute: false }) public hass?: HomeAssistant; | ||||||
|  |  | ||||||
|   @property({ attribute: false }) public stateObj?: CameraEntity; |   @property() public stateObj?: CameraEntity; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public showControls = false; |   @property({ type: Boolean }) public showControls = false; | ||||||
|  |  | ||||||
|  |   @internalProperty() private _attached = false; | ||||||
|  |  | ||||||
|   // We keep track if we should force MJPEG with a string |   // We keep track if we should force MJPEG with a string | ||||||
|   // that way it automatically resets if we change entity. |   // that way it automatically resets if we change entity. | ||||||
|   @internalProperty() private _forceMJPEG?: string; |   @internalProperty() private _forceMJPEG: string | undefined = undefined; | ||||||
|  |  | ||||||
|   @internalProperty() private _url?: string; |   private _hlsPolyfillInstance?: Hls; | ||||||
|  |  | ||||||
|  |   public connectedCallback() { | ||||||
|  |     super.connectedCallback(); | ||||||
|  |     this._attached = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public disconnectedCallback() { | ||||||
|  |     super.disconnectedCallback(); | ||||||
|  |     this._attached = false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     if (!this.stateObj) { |     if (!this.stateObj || !this._attached) { | ||||||
|       return html``; |       return html``; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -52,25 +65,51 @@ class HaCameraStream extends LitElement { | |||||||
|               )} camera.`} |               )} camera.`} | ||||||
|             /> |             /> | ||||||
|           ` |           ` | ||||||
|         : this._url |         : html` | ||||||
|         ? html` |             <video | ||||||
|             <ha-hls-player |  | ||||||
|               autoplay |               autoplay | ||||||
|               muted |               muted | ||||||
|               playsinline |               playsinline | ||||||
|               ?controls=${this.showControls} |               ?controls=${this.showControls} | ||||||
|               .hass=${this.hass} |               @loadeddata=${this._elementResized} | ||||||
|               .url=${this._url} |             ></video> | ||||||
|             ></ha-hls-player> |           `} | ||||||
|           ` |  | ||||||
|         : ""} |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected updated(changedProps: PropertyValues): void { |   protected updated(changedProps: PropertyValues) { | ||||||
|     if (changedProps.has("stateObj") && !this._shouldRenderMJPEG) { |     super.updated(changedProps); | ||||||
|       this._forceMJPEG = undefined; |  | ||||||
|       this._getStreamUrl(); |     const stateObjChanged = changedProps.has("stateObj"); | ||||||
|  |     const attachedChanged = changedProps.has("_attached"); | ||||||
|  |  | ||||||
|  |     const oldState = changedProps.get("stateObj") as this["stateObj"]; | ||||||
|  |     const oldEntityId = oldState ? oldState.entity_id : undefined; | ||||||
|  |     const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined; | ||||||
|  |  | ||||||
|  |     if ( | ||||||
|  |       (!stateObjChanged && !attachedChanged) || | ||||||
|  |       (stateObjChanged && oldEntityId === curEntityId) | ||||||
|  |     ) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // If we are no longer attached, destroy polyfill. | ||||||
|  |     if (attachedChanged && !this._attached) { | ||||||
|  |       this._destroyPolyfill(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Nothing to do if we are render MJPEG. | ||||||
|  |     if (this._shouldRenderMJPEG) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Tear down existing polyfill, if available | ||||||
|  |     this._destroyPolyfill(); | ||||||
|  |  | ||||||
|  |     if (curEntityId) { | ||||||
|  |       this._startHls(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -82,35 +121,96 @@ class HaCameraStream extends LitElement { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _getStreamUrl(): Promise<void> { |   private get _videoEl(): HTMLVideoElement { | ||||||
|  |     return this.shadowRoot!.querySelector("video")!; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _startHls(): Promise<void> { | ||||||
|  |     // eslint-disable-next-line | ||||||
|  |     const Hls = ((await import( | ||||||
|  |       /* webpackChunkName: "hls.js" */ "hls.js" | ||||||
|  |     )) as any).default as HLSModule; | ||||||
|  |     let hlsSupported = Hls.isSupported(); | ||||||
|  |     const videoEl = this._videoEl; | ||||||
|  |  | ||||||
|  |     if (!hlsSupported) { | ||||||
|  |       hlsSupported = | ||||||
|  |         videoEl.canPlayType("application/vnd.apple.mpegurl") !== ""; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!hlsSupported) { | ||||||
|  |       this._forceMJPEG = this.stateObj!.entity_id; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       const { url } = await fetchStreamUrl( |       const { url } = await fetchStreamUrl( | ||||||
|         this.hass!, |         this.hass!, | ||||||
|         this.stateObj!.entity_id |         this.stateObj!.entity_id | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       this._url = url; |       if (Hls.isSupported()) { | ||||||
|  |         this._renderHLSPolyfill(videoEl, Hls, url); | ||||||
|  |       } else { | ||||||
|  |         this._renderHLSNative(videoEl, url); | ||||||
|  |       } | ||||||
|  |       return; | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       // Fails if we were unable to get a stream |       // Fails if we were unable to get a stream | ||||||
|       // eslint-disable-next-line |       // eslint-disable-next-line | ||||||
|       console.error(err); |       console.error(err); | ||||||
|  |  | ||||||
|       this._forceMJPEG = this.stateObj!.entity_id; |       this._forceMJPEG = this.stateObj!.entity_id; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) { | ||||||
|  |     videoEl.src = url; | ||||||
|  |     await new Promise((resolve) => | ||||||
|  |       videoEl.addEventListener("loadedmetadata", resolve) | ||||||
|  |     ); | ||||||
|  |     videoEl.play(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _renderHLSPolyfill( | ||||||
|  |     videoEl: HTMLVideoElement, | ||||||
|  |     // eslint-disable-next-line | ||||||
|  |     Hls: HLSModule, | ||||||
|  |     url: string | ||||||
|  |   ) { | ||||||
|  |     const hls = new Hls({ | ||||||
|  |       liveBackBufferLength: 60, | ||||||
|  |       fragLoadingTimeOut: 30000, | ||||||
|  |       manifestLoadingTimeOut: 30000, | ||||||
|  |       levelLoadingTimeOut: 30000, | ||||||
|  |     }); | ||||||
|  |     this._hlsPolyfillInstance = hls; | ||||||
|  |     hls.attachMedia(videoEl); | ||||||
|  |     hls.on(Hls.Events.MEDIA_ATTACHED, () => { | ||||||
|  |       hls.loadSource(url); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private _elementResized() { |   private _elementResized() { | ||||||
|     fireEvent(this, "iron-resize"); |     fireEvent(this, "iron-resize"); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _destroyPolyfill(): void { | ||||||
|  |     if (this._hlsPolyfillInstance) { | ||||||
|  |       this._hlsPolyfillInstance.destroy(); | ||||||
|  |       this._hlsPolyfillInstance = undefined; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResult { |   static get styles(): CSSResult { | ||||||
|     return css` |     return css` | ||||||
|       :host, |       :host, | ||||||
|       img { |       img, | ||||||
|  |       video { | ||||||
|         display: block; |         display: block; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       img { |       img, | ||||||
|  |       video { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -1,16 +1,16 @@ | |||||||
| // @ts-ignore |  | ||||||
| import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css"; |  | ||||||
| import { | import { | ||||||
|   css, |  | ||||||
|   customElement, |  | ||||||
|   html, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|  |   TemplateResult, | ||||||
|   property, |   property, | ||||||
|   svg, |   svg, | ||||||
|   SVGTemplateResult, |   html, | ||||||
|   TemplateResult, |   customElement, | ||||||
|   unsafeCSS, |   unsafeCSS, | ||||||
|  |   SVGTemplateResult, | ||||||
|  |   css, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
|  | // @ts-ignore | ||||||
|  | import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css"; | ||||||
| import { classMap } from "lit-html/directives/class-map"; | import { classMap } from "lit-html/directives/class-map"; | ||||||
|  |  | ||||||
| @customElement("ha-circular-progress") | @customElement("ha-circular-progress") | ||||||
| @@ -24,7 +24,7 @@ export class HaCircularProgress extends LitElement { | |||||||
|   @property() |   @property() | ||||||
|   public size: "small" | "medium" | "large" = "medium"; |   public size: "small" | "medium" | "large" = "medium"; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult | void { | ||||||
|     let indeterminatePart: SVGTemplateResult; |     let indeterminatePart: SVGTemplateResult; | ||||||
|  |  | ||||||
|     if (this.size === "small") { |     if (this.size === "small") { | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| import { Editor } from "codemirror"; | import { Editor } from "codemirror"; | ||||||
| import { | import { | ||||||
|   customElement, |   customElement, | ||||||
|   internalProperty, |  | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   PropertyValues, |   PropertyValues, | ||||||
|   UpdatingElement, |   UpdatingElement, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| @@ -101,6 +101,11 @@ export class HaCodeEditor extends UpdatingElement { | |||||||
|       .CodeMirror-scroll { |       .CodeMirror-scroll { | ||||||
|         max-height: var(--code-mirror-max-height, --code-mirror-height); |         max-height: var(--code-mirror-max-height, --code-mirror-height); | ||||||
|       } |       } | ||||||
|  |       .CodeMirror-gutters { | ||||||
|  |         border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color)); | ||||||
|  |         background-color: var(--paper-dialog-background-color, var(--primary-background-color)); | ||||||
|  |         transition: 0.2s ease border-right; | ||||||
|  |       } | ||||||
|       :host(.error-state) .CodeMirror-gutters { |       :host(.error-state) .CodeMirror-gutters { | ||||||
|         border-color: var(--error-state-color, red); |         border-color: var(--error-state-color, red); | ||||||
|       } |       } | ||||||
| @@ -108,7 +113,7 @@ export class HaCodeEditor extends UpdatingElement { | |||||||
|         border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color)); |         border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color)); | ||||||
|       } |       } | ||||||
|       .CodeMirror-linenumber { |       .CodeMirror-linenumber { | ||||||
|         color: var(--paper-dialog-color, var(--secondary-text-color)); |         color: var(--paper-dialog-color, var(--primary-text-color)); | ||||||
|       } |       } | ||||||
|       .rtl .CodeMirror-vscrollbar { |       .rtl .CodeMirror-vscrollbar { | ||||||
|         right: auto; |         right: auto; | ||||||
| @@ -117,100 +122,6 @@ export class HaCodeEditor extends UpdatingElement { | |||||||
|       .rtl-gutter { |       .rtl-gutter { | ||||||
|         width: 20px; |         width: 20px; | ||||||
|       } |       } | ||||||
|       .CodeMirror-gutters { |  | ||||||
|         border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color)); |  | ||||||
|         background-color: var(--paper-dialog-background-color, var(--primary-background-color)); |  | ||||||
|         transition: 0.2s ease border-right; |  | ||||||
|       } |  | ||||||
|       .cm-s-default.CodeMirror { |  | ||||||
|         background-color: var(--code-editor-background-color, var(--card-background-color)); |  | ||||||
|         color: var(--primary-text-color); |  | ||||||
|       } |  | ||||||
|       .cm-s-default .CodeMirror-cursor { |  | ||||||
|         border-left: 1px solid var(--secondary-text-color); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default div.CodeMirror-selected, .cm-s-default.CodeMirror-focused div.CodeMirror-selected { |  | ||||||
|         background: rgba(var(--rgb-primary-color), 0.2); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .CodeMirror-line::selection, |  | ||||||
|       .cm-s-default .CodeMirror-line>span::selection, |  | ||||||
|       .cm-s-default .CodeMirror-line>span>span::selection { |  | ||||||
|         background: rgba(var(--rgb-primary-color), 0.2); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-keyword { |  | ||||||
|         color: var(--codemirror-keyword, #6262FF); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-operator { |  | ||||||
|         color: var(--codemirror-operator, #cda869); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-variable-2 { |  | ||||||
|         color: var(--codemirror-variable-2, #690); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-builtin { |  | ||||||
|         color: var(--codemirror-builtin, #9B7536); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-atom { |  | ||||||
|         color: var(--codemirror-atom, #F90); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-number { |  | ||||||
|         color: var(--codemirror-number, #ca7841); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-def { |  | ||||||
|         color: var(--codemirror-def, #8DA6CE); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-string { |  | ||||||
|         color: var(--codemirror-string, #07a); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-string-2 { |  | ||||||
|         color: var(--codemirror-string-2, #bd6b18); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-comment { |  | ||||||
|         color: var(--codemirror-comment, #777); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-variable { |  | ||||||
|         color: var(--codemirror-variable, #07a); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-tag { |  | ||||||
|         color: var(--codemirror-tag, #997643); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-meta { |  | ||||||
|         color: var(--codemirror-meta, #000); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-attribute { |  | ||||||
|         color: var(--codemirror-attribute, #d6bb6d); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-property { |  | ||||||
|         color: var(--codemirror-property, #905); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-qualifier { |  | ||||||
|         color: var(--codemirror-qualifier, #690); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .cm-s-default .cm-variable-3  { |  | ||||||
|         color: var(--codemirror-variable-3, #07a); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .cm-s-default .cm-type { |  | ||||||
|         color: var(--codemirror-type, #07a); |  | ||||||
|       } |  | ||||||
|     </style>`; |     </style>`; | ||||||
|  |  | ||||||
|     this.codemirror = codeMirror(shadowRoot, { |     this.codemirror = codeMirror(shadowRoot, { | ||||||
|   | |||||||
| @@ -176,11 +176,6 @@ class HaColorPicker extends EventsMixin(PolymerElement) { | |||||||
|     this.drawColorWheel(); |     this.drawColorWheel(); | ||||||
|     this.drawMarker(); |     this.drawMarker(); | ||||||
|  |  | ||||||
|     if (this.desiredHsColor) { |  | ||||||
|       this.setMarkerOnColor(this.desiredHsColor); |  | ||||||
|       this.applyColorToCanvas(this.desiredHsColor); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.interactionLayer.addEventListener("mousedown", (ev) => |     this.interactionLayer.addEventListener("mousedown", (ev) => | ||||||
|       this.onMouseDown(ev) |       this.onMouseDown(ev) | ||||||
|     ); |     ); | ||||||
| @@ -324,9 +319,6 @@ class HaColorPicker extends EventsMixin(PolymerElement) { | |||||||
|  |  | ||||||
|   // set marker position to the given color |   // set marker position to the given color | ||||||
|   setMarkerOnColor(hs) { |   setMarkerOnColor(hs) { | ||||||
|     if (!this.marker || !this.tooltip) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     const dist = hs.s * this.radius; |     const dist = hs.s * this.radius; | ||||||
|     const theta = ((hs.h - 180) / 180) * Math.PI; |     const theta = ((hs.h - 180) / 180) * Math.PI; | ||||||
|     const markerdX = -dist * Math.cos(theta); |     const markerdX = -dist * Math.cos(theta); | ||||||
| @@ -338,9 +330,6 @@ class HaColorPicker extends EventsMixin(PolymerElement) { | |||||||
|  |  | ||||||
|   // apply given color to interface elements |   // apply given color to interface elements | ||||||
|   applyColorToCanvas(hs) { |   applyColorToCanvas(hs) { | ||||||
|     if (!this.interactionLayer) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     // we're not really converting hs to hsl here, but we keep it cheap |     // we're not really converting hs to hsl here, but we keep it cheap | ||||||
|     // setting the color on the interactionLayer, the svg elements can inherit |     // setting the color on the interactionLayer, the svg elements can inherit | ||||||
|     this.interactionLayer.style.color = `hsl(${hs.h}, 100%, ${ |     this.interactionLayer.style.color = `hsl(${hs.h}, 100%, ${ | ||||||
|   | |||||||
| @@ -1,16 +1,16 @@ | |||||||
| import "@material/mwc-dialog"; | import "@material/mwc-dialog"; | ||||||
| import type { Dialog } from "@material/mwc-dialog"; | import type { Dialog } from "@material/mwc-dialog"; | ||||||
| import { style } from "@material/mwc-dialog/mwc-dialog-css"; | import { style } from "@material/mwc-dialog/mwc-dialog-css"; | ||||||
| import { mdiClose } from "@mdi/js"; |  | ||||||
| import { css, CSSResult, customElement, html } from "lit-element"; |  | ||||||
| import { computeRTLDirection } from "../common/util/compute_rtl"; |  | ||||||
| import type { Constructor, HomeAssistant } from "../types"; |  | ||||||
| import "./ha-icon-button"; | import "./ha-icon-button"; | ||||||
|  | import { css, CSSResult, customElement, html } from "lit-element"; | ||||||
|  | import type { Constructor, HomeAssistant } from "../types"; | ||||||
|  | import { mdiClose } from "@mdi/js"; | ||||||
|  | import { computeRTLDirection } from "../common/util/compute_rtl"; | ||||||
|  |  | ||||||
| const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>; | const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>; | ||||||
|  |  | ||||||
| export const createCloseHeading = (hass: HomeAssistant, title: string) => html` | export const createCloseHeading = (hass: HomeAssistant, title: string) => html` | ||||||
|   <span class="header_title">${title}</span> |   ${title} | ||||||
|   <mwc-icon-button |   <mwc-icon-button | ||||||
|     aria-label=${hass.localize("ui.dialogs.generic.close")} |     aria-label=${hass.localize("ui.dialogs.generic.close")} | ||||||
|     dialogAction="close" |     dialogAction="close" | ||||||
| @@ -23,10 +23,6 @@ export const createCloseHeading = (hass: HomeAssistant, title: string) => html` | |||||||
|  |  | ||||||
| @customElement("ha-dialog") | @customElement("ha-dialog") | ||||||
| export class HaDialog extends MwcDialog { | export class HaDialog extends MwcDialog { | ||||||
|   public scrollToPos(x: number, y: number) { |  | ||||||
|     this.contentElement.scrollTo(x, y); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected renderHeading() { |   protected renderHeading() { | ||||||
|     return html`<slot name="heading"> |     return html`<slot name="heading"> | ||||||
|       ${super.renderHeading()} |       ${super.renderHeading()} | ||||||
| @@ -64,13 +60,8 @@ export class HaDialog extends MwcDialog { | |||||||
|         } |         } | ||||||
|         .mdc-dialog .mdc-dialog__surface { |         .mdc-dialog .mdc-dialog__surface { | ||||||
|           position: var(--dialog-surface-position, relative); |           position: var(--dialog-surface-position, relative); | ||||||
|           top: var(--dialog-surface-top); |  | ||||||
|           min-height: var(--mdc-dialog-min-height, auto); |           min-height: var(--mdc-dialog-min-height, auto); | ||||||
|         } |         } | ||||||
|         :host([flexContent]) .mdc-dialog .mdc-dialog__content { |  | ||||||
|           display: flex; |  | ||||||
|           flex-direction: column; |  | ||||||
|         } |  | ||||||
|         .header_button { |         .header_button { | ||||||
|           position: absolute; |           position: absolute; | ||||||
|           right: 16px; |           right: 16px; | ||||||
| @@ -78,17 +69,10 @@ export class HaDialog extends MwcDialog { | |||||||
|           text-decoration: none; |           text-decoration: none; | ||||||
|           color: inherit; |           color: inherit; | ||||||
|         } |         } | ||||||
|         .header_title { |  | ||||||
|           margin-right: 40px; |  | ||||||
|         } |  | ||||||
|         [dir="rtl"].header_button { |         [dir="rtl"].header_button { | ||||||
|           right: auto; |           right: auto; | ||||||
|           left: 16px; |           left: 16px; | ||||||
|         } |         } | ||||||
|         [dir="rtl"].header_title { |  | ||||||
|           margin-left: 40px; |  | ||||||
|           margin-right: 0px; |  | ||||||
|         } |  | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -54,8 +54,7 @@ export class HaFormInteger extends LitElement implements HaFormElement { | |||||||
|                   ` |                   ` | ||||||
|                 : ""} |                 : ""} | ||||||
|               <ha-paper-slider |               <ha-paper-slider | ||||||
|                 pin |                 pin="" | ||||||
|                 editable |  | ||||||
|                 .value=${this._value} |                 .value=${this._value} | ||||||
|                 .min=${this.schema.valueMin} |                 .min=${this.schema.valueMin} | ||||||
|                 .max=${this.schema.valueMax} |                 .max=${this.schema.valueMax} | ||||||
| @@ -112,10 +111,6 @@ export class HaFormInteger extends LitElement implements HaFormElement { | |||||||
|       .flex { |       .flex { | ||||||
|         display: flex; |         display: flex; | ||||||
|       } |       } | ||||||
|       ha-paper-slider { |  | ||||||
|         width: 100%; |  | ||||||
|         margin-right: 16px; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; | ||||||
| import "@polymer/paper-item/paper-item"; | import "@polymer/paper-item/paper-item"; | ||||||
| import "@polymer/paper-listbox/paper-listbox"; | import "@polymer/paper-listbox/paper-listbox"; | ||||||
| import { | import { | ||||||
| @@ -11,7 +12,6 @@ import { | |||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import "../ha-paper-dropdown-menu"; |  | ||||||
| import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form"; | import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form"; | ||||||
|  |  | ||||||
| @customElement("ha-form-select") | @customElement("ha-form-select") | ||||||
| @@ -24,7 +24,7 @@ export class HaFormSelect extends LitElement implements HaFormElement { | |||||||
|  |  | ||||||
|   @property() public suffix!: string; |   @property() public suffix!: string; | ||||||
|  |  | ||||||
|   @query("ha-paper-dropdown-menu") private _input?: HTMLElement; |   @query("paper-dropdown-menu") private _input?: HTMLElement; | ||||||
|  |  | ||||||
|   public focus() { |   public focus() { | ||||||
|     if (this._input) { |     if (this._input) { | ||||||
| @@ -34,7 +34,7 @@ export class HaFormSelect extends LitElement implements HaFormElement { | |||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <ha-paper-dropdown-menu .label=${this.label}> |       <paper-dropdown-menu .label=${this.label}> | ||||||
|         <paper-listbox |         <paper-listbox | ||||||
|           slot="dropdown-content" |           slot="dropdown-content" | ||||||
|           attr-for-selected="item-value" |           attr-for-selected="item-value" | ||||||
| @@ -51,7 +51,7 @@ export class HaFormSelect extends LitElement implements HaFormElement { | |||||||
|             ` |             ` | ||||||
|           )} |           )} | ||||||
|         </paper-listbox> |         </paper-listbox> | ||||||
|       </ha-paper-dropdown-menu> |       </paper-dropdown-menu> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -74,7 +74,7 @@ export class HaFormSelect extends LitElement implements HaFormElement { | |||||||
|  |  | ||||||
|   static get styles(): CSSResult { |   static get styles(): CSSResult { | ||||||
|     return css` |     return css` | ||||||
|       ha-paper-dropdown-menu { |       paper-dropdown-menu { | ||||||
|         display: block; |         display: block; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -37,6 +37,24 @@ export class HaFormString extends LitElement implements HaFormElement { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   protected firstUpdated(): void { | ||||||
|  |     if (this.schema.name.includes("password")) { | ||||||
|  |       const stepInput = document.createElement("input"); | ||||||
|  |       stepInput.setAttribute("type", "password"); | ||||||
|  |       stepInput.setAttribute("name", "password"); | ||||||
|  |       stepInput.setAttribute("autocomplete", "on"); | ||||||
|  |       stepInput.onkeyup = (ev) => this._externalValueChanged(ev, this); | ||||||
|  |       document.documentElement.appendChild(stepInput); | ||||||
|  |     } else if (this.schema.name.includes("username")) { | ||||||
|  |       const stepInput = document.createElement("input"); | ||||||
|  |       stepInput.setAttribute("type", "text"); | ||||||
|  |       stepInput.setAttribute("name", "username"); | ||||||
|  |       stepInput.setAttribute("autocomplete", "on"); | ||||||
|  |       stepInput.onkeyup = (ev) => this._externalValueChanged(ev, this); | ||||||
|  |       document.documentElement.appendChild(stepInput); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return this.schema.name.includes("password") |     return this.schema.name.includes("password") | ||||||
|       ? html` |       ? html` | ||||||
| @@ -81,11 +99,21 @@ export class HaFormString extends LitElement implements HaFormElement { | |||||||
|     if (this.data === value) { |     if (this.data === value) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fireEvent(this, "value-changed", { |     fireEvent(this, "value-changed", { | ||||||
|       value, |       value, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _externalValueChanged(ev: Event, el): void { | ||||||
|  |     const value = (ev.target as PaperInputElement).value; | ||||||
|  |     if (this.data === value) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     el.shadowRoot!.querySelector("paper-input").value = value; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private get _stringType(): string { |   private get _stringType(): string { | ||||||
|     if (this.schema.format) { |     if (this.schema.format) { | ||||||
|       if (["email", "url"].includes(this.schema.format)) { |       if (["email", "url"].includes(this.schema.format)) { | ||||||
|   | |||||||
| @@ -11,13 +11,23 @@ import { styleMap } from "lit-html/directives/style-map"; | |||||||
| import { afterNextRender } from "../common/util/render-status"; | import { afterNextRender } from "../common/util/render-status"; | ||||||
| import { ifDefined } from "lit-html/directives/if-defined"; | import { ifDefined } from "lit-html/directives/if-defined"; | ||||||
|  |  | ||||||
| import { getValueInPercentage, normalize } from "../util/calculate"; |  | ||||||
|  |  | ||||||
| const getAngle = (value: number, min: number, max: number) => { | const getAngle = (value: number, min: number, max: number) => { | ||||||
|   const percentage = getValueInPercentage(normalize(value, min, max), min, max); |   const percentage = getValueInPercentage(normalize(value, min, max), min, max); | ||||||
|   return (percentage * 180) / 100; |   return (percentage * 180) / 100; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const normalize = (value: number, min: number, max: number) => { | ||||||
|  |   if (value > max) return max; | ||||||
|  |   if (value < min) return min; | ||||||
|  |   return value; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const getValueInPercentage = (value: number, min: number, max: number) => { | ||||||
|  |   const newMax = max - min; | ||||||
|  |   const newVal = value - min; | ||||||
|  |   return (100 * newVal) / newMax; | ||||||
|  | }; | ||||||
|  |  | ||||||
| // Workaround for https://github.com/home-assistant/frontend/issues/6467 | // Workaround for https://github.com/home-assistant/frontend/issues/6467 | ||||||
| const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); | const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
|  | import { customElement, LitElement, html, unsafeCSS, css } from "lit-element"; | ||||||
| // @ts-ignore | // @ts-ignore | ||||||
| import topAppBarStyles from "@material/top-app-bar/dist/mdc.top-app-bar.min.css"; | import topAppBarStyles from "@material/top-app-bar/dist/mdc.top-app-bar.min.css"; | ||||||
| import { css, customElement, html, LitElement, unsafeCSS } from "lit-element"; |  | ||||||
|  |  | ||||||
| @customElement("ha-header-bar") | @customElement("ha-header-bar") | ||||||
| export class HaHeaderBar extends LitElement { | export class HaHeaderBar extends LitElement { | ||||||
|   | |||||||
| @@ -1,216 +0,0 @@ | |||||||
| import { |  | ||||||
|   css, |  | ||||||
|   CSSResult, |  | ||||||
|   customElement, |  | ||||||
|   html, |  | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |  | ||||||
|   property, |  | ||||||
|   PropertyValues, |  | ||||||
|   query, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit-element"; |  | ||||||
| import { fireEvent } from "../common/dom/fire_event"; |  | ||||||
| import { nextRender } from "../common/util/render-status"; |  | ||||||
| import { getExternalConfig } from "../external_app/external_config"; |  | ||||||
| import type { HomeAssistant } from "../types"; |  | ||||||
|  |  | ||||||
| type HLSModule = typeof import("hls.js"); |  | ||||||
|  |  | ||||||
| @customElement("ha-hls-player") |  | ||||||
| class HaHLSPlayer extends LitElement { |  | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   @property() public url!: string; |  | ||||||
|  |  | ||||||
|   @property({ type: Boolean, attribute: "controls" }) |  | ||||||
|   public controls = false; |  | ||||||
|  |  | ||||||
|   @property({ type: Boolean, attribute: "muted" }) |  | ||||||
|   public muted = false; |  | ||||||
|  |  | ||||||
|   @property({ type: Boolean, attribute: "autoplay" }) |  | ||||||
|   public autoPlay = false; |  | ||||||
|  |  | ||||||
|   @property({ type: Boolean, attribute: "playsinline" }) |  | ||||||
|   public playsInline = false; |  | ||||||
|  |  | ||||||
|   @query("video") private _videoEl!: HTMLVideoElement; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _attached = false; |  | ||||||
|  |  | ||||||
|   private _hlsPolyfillInstance?: Hls; |  | ||||||
|  |  | ||||||
|   private _useExoPlayer = false; |  | ||||||
|  |  | ||||||
|   public connectedCallback() { |  | ||||||
|     super.connectedCallback(); |  | ||||||
|     this._attached = true; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public disconnectedCallback() { |  | ||||||
|     super.disconnectedCallback(); |  | ||||||
|     this._attached = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |  | ||||||
|     if (!this._attached) { |  | ||||||
|       return html``; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return html` |  | ||||||
|       <video |  | ||||||
|         ?autoplay=${this.autoPlay} |  | ||||||
|         ?muted=${this.muted} |  | ||||||
|         ?playsinline=${this.playsInline} |  | ||||||
|         ?controls=${this.controls} |  | ||||||
|         @loadeddata=${this._elementResized} |  | ||||||
|       ></video> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected updated(changedProps: PropertyValues) { |  | ||||||
|     super.updated(changedProps); |  | ||||||
|  |  | ||||||
|     const attachedChanged = changedProps.has("_attached"); |  | ||||||
|     const urlChanged = changedProps.has("url"); |  | ||||||
|  |  | ||||||
|     if (!urlChanged && !attachedChanged) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // If we are no longer attached, destroy polyfill |  | ||||||
|     if (attachedChanged && !this._attached) { |  | ||||||
|       // Tear down existing polyfill, if available |  | ||||||
|       this._destroyPolyfill(); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this._destroyPolyfill(); |  | ||||||
|     this._startHls(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _getUseExoPlayer(): Promise<boolean> { |  | ||||||
|     if (!this.hass!.auth.external) { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|     const externalConfig = await getExternalConfig(this.hass!.auth.external); |  | ||||||
|     return externalConfig && externalConfig.hasExoPlayer; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _startHls(): Promise<void> { |  | ||||||
|     let hls: any; |  | ||||||
|     const videoEl = this._videoEl; |  | ||||||
|     this._useExoPlayer = await this._getUseExoPlayer(); |  | ||||||
|     if (!this._useExoPlayer) { |  | ||||||
|       hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any) |  | ||||||
|         .default as HLSModule; |  | ||||||
|       let hlsSupported = hls.isSupported(); |  | ||||||
|  |  | ||||||
|       if (!hlsSupported) { |  | ||||||
|         hlsSupported = |  | ||||||
|           videoEl.canPlayType("application/vnd.apple.mpegurl") !== ""; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (!hlsSupported) { |  | ||||||
|         this._videoEl.innerHTML = this.hass.localize( |  | ||||||
|           "ui.components.media-browser.video_not_supported" |  | ||||||
|         ); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const url = this.url; |  | ||||||
|  |  | ||||||
|     if (this._useExoPlayer) { |  | ||||||
|       this._renderHLSExoPlayer(url); |  | ||||||
|     } else if (hls.isSupported()) { |  | ||||||
|       this._renderHLSPolyfill(videoEl, hls, url); |  | ||||||
|     } else { |  | ||||||
|       this._renderHLSNative(videoEl, url); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _renderHLSExoPlayer(url: string) { |  | ||||||
|     window.addEventListener("resize", this._resizeExoPlayer); |  | ||||||
|     this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer); |  | ||||||
|     this._videoEl.style.visibility = "hidden"; |  | ||||||
|     await this.hass!.auth.external!.sendMessage({ |  | ||||||
|       type: "exoplayer/play_hls", |  | ||||||
|       payload: new URL(url, window.location.href).toString(), |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _resizeExoPlayer = () => { |  | ||||||
|     const rect = this._videoEl.getBoundingClientRect(); |  | ||||||
|     this.hass!.auth.external!.fireMessage({ |  | ||||||
|       type: "exoplayer/resize", |  | ||||||
|       payload: { |  | ||||||
|         left: rect.left, |  | ||||||
|         top: rect.top, |  | ||||||
|         right: rect.right, |  | ||||||
|         bottom: rect.bottom, |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   private async _renderHLSPolyfill( |  | ||||||
|     videoEl: HTMLVideoElement, |  | ||||||
|     Hls: HLSModule, |  | ||||||
|     url: string |  | ||||||
|   ) { |  | ||||||
|     const hls = new Hls({ |  | ||||||
|       liveBackBufferLength: 60, |  | ||||||
|       fragLoadingTimeOut: 30000, |  | ||||||
|       manifestLoadingTimeOut: 30000, |  | ||||||
|       levelLoadingTimeOut: 30000, |  | ||||||
|     }); |  | ||||||
|     this._hlsPolyfillInstance = hls; |  | ||||||
|     hls.attachMedia(videoEl); |  | ||||||
|     hls.on(Hls.Events.MEDIA_ATTACHED, () => { |  | ||||||
|       hls.loadSource(url); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) { |  | ||||||
|     videoEl.src = url; |  | ||||||
|     await new Promise((resolve) => |  | ||||||
|       videoEl.addEventListener("loadedmetadata", resolve) |  | ||||||
|     ); |  | ||||||
|     videoEl.play(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _elementResized() { |  | ||||||
|     fireEvent(this, "iron-resize"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _destroyPolyfill() { |  | ||||||
|     if (this._hlsPolyfillInstance) { |  | ||||||
|       this._hlsPolyfillInstance.destroy(); |  | ||||||
|       this._hlsPolyfillInstance = undefined; |  | ||||||
|     } |  | ||||||
|     if (this._useExoPlayer) { |  | ||||||
|       window.removeEventListener("resize", this._resizeExoPlayer); |  | ||||||
|       this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult { |  | ||||||
|     return css` |  | ||||||
|       :host, |  | ||||||
|       video { |  | ||||||
|         display: block; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       video { |  | ||||||
|         width: 100%; |  | ||||||
|       } |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "ha-hls-player": HaHLSPlayer; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -106,7 +106,6 @@ const mdiRenameMapping = { | |||||||
|   pot: "pot-steam", |   pot: "pot-steam", | ||||||
|   ruby: "language-ruby", |   ruby: "language-ruby", | ||||||
|   sailing: "sail-boat", |   sailing: "sail-boat", | ||||||
|   scooter: "human-scooter", |  | ||||||
|   settings: "cog", |   settings: "cog", | ||||||
|   "settings-box": "cog-box", |   "settings-box": "cog-box", | ||||||
|   "settings-outline": "cog-outline", |   "settings-outline": "cog-outline", | ||||||
|   | |||||||
| @@ -68,10 +68,6 @@ class HaPaperSlider extends PaperSliderClass { | |||||||
|         -webkit-transform: scale(1) translate(0, -17px) scaleX(-1) !important; |         -webkit-transform: scale(1) translate(0, -17px) scaleX(-1) !important; | ||||||
|         transform: scale(1) translate(0, -17px) scaleX(-1) !important; |         transform: scale(1) translate(0, -17px) scaleX(-1) !important; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .slider-input { |  | ||||||
|           width: 54px; |  | ||||||
|         } |  | ||||||
|     `; |     `; | ||||||
|     tpl.content.appendChild(styleEl); |     tpl.content.appendChild(styleEl); | ||||||
|     return tpl; |     return tpl; | ||||||
|   | |||||||
| @@ -1,226 +0,0 @@ | |||||||
| import "@material/mwc-icon-button/mwc-icon-button"; |  | ||||||
| import { mdiClose, mdiImagePlus } from "@mdi/js"; |  | ||||||
| import "@polymer/iron-input/iron-input"; |  | ||||||
| import "@polymer/paper-input/paper-input-container"; |  | ||||||
| import { |  | ||||||
|   css, |  | ||||||
|   customElement, |  | ||||||
|   html, |  | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |  | ||||||
|   property, |  | ||||||
|   PropertyValues, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit-element"; |  | ||||||
| import { classMap } from "lit-html/directives/class-map"; |  | ||||||
| import { fireEvent } from "../common/dom/fire_event"; |  | ||||||
| import { createImage, generateImageThumbnailUrl } from "../data/image"; |  | ||||||
| import { HomeAssistant } from "../types"; |  | ||||||
| import "./ha-circular-progress"; |  | ||||||
| import "./ha-svg-icon"; |  | ||||||
| import { |  | ||||||
|   showImageCropperDialog, |  | ||||||
|   CropOptions, |  | ||||||
| } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog"; |  | ||||||
|  |  | ||||||
| @customElement("ha-picture-upload") |  | ||||||
| export class HaPictureUpload extends LitElement { |  | ||||||
|   public hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   @property() public value: string | null = null; |  | ||||||
|  |  | ||||||
|   @property() public label?: string; |  | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public crop = false; |  | ||||||
|  |  | ||||||
|   @property({ attribute: false }) public cropOptions?: CropOptions; |  | ||||||
|  |  | ||||||
|   @property({ type: Number }) public size = 512; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _error = ""; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _uploading = false; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _drag = false; |  | ||||||
|  |  | ||||||
|   protected updated(changedProperties: PropertyValues) { |  | ||||||
|     if (changedProperties.has("_drag")) { |  | ||||||
|       (this.shadowRoot!.querySelector( |  | ||||||
|         "paper-input-container" |  | ||||||
|       ) as any)._setFocused(this._drag); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public render(): TemplateResult { |  | ||||||
|     return html` |  | ||||||
|       ${this._uploading |  | ||||||
|         ? html`<ha-circular-progress |  | ||||||
|             alt="Uploading" |  | ||||||
|             size="large" |  | ||||||
|             active |  | ||||||
|           ></ha-circular-progress>` |  | ||||||
|         : html` |  | ||||||
|             ${this._error ? html`<div class="error">${this._error}</div>` : ""} |  | ||||||
|             <label for="input"> |  | ||||||
|               <paper-input-container |  | ||||||
|                 .alwaysFloatLabel=${Boolean(this.value)} |  | ||||||
|                 @drop=${this._handleDrop} |  | ||||||
|                 @dragenter=${this._handleDragStart} |  | ||||||
|                 @dragover=${this._handleDragStart} |  | ||||||
|                 @dragleave=${this._handleDragEnd} |  | ||||||
|                 @dragend=${this._handleDragEnd} |  | ||||||
|                 class=${classMap({ |  | ||||||
|                   dragged: this._drag, |  | ||||||
|                 })} |  | ||||||
|               > |  | ||||||
|                 <label for="input" slot="label"> |  | ||||||
|                   ${this.label || |  | ||||||
|                   this.hass.localize("ui.components.picture-upload.label")} |  | ||||||
|                 </label> |  | ||||||
|                 <iron-input slot="input"> |  | ||||||
|                   <input |  | ||||||
|                     id="input" |  | ||||||
|                     type="file" |  | ||||||
|                     class="file" |  | ||||||
|                     accept="image/png, image/jpeg, image/gif" |  | ||||||
|                     @change=${this._handleFilePicked} |  | ||||||
|                   /> |  | ||||||
|                   ${this.value ? html`<img .src=${this.value} />` : ""} |  | ||||||
|                 </iron-input> |  | ||||||
|                 ${this.value |  | ||||||
|                   ? html`<mwc-icon-button |  | ||||||
|                       slot="suffix" |  | ||||||
|                       @click=${this._clearPicture} |  | ||||||
|                     > |  | ||||||
|                       <ha-svg-icon .path=${mdiClose}></ha-svg-icon> |  | ||||||
|                     </mwc-icon-button>` |  | ||||||
|                   : html`<mwc-icon-button slot="suffix"> |  | ||||||
|                       <ha-svg-icon .path=${mdiImagePlus}></ha-svg-icon> |  | ||||||
|                     </mwc-icon-button>`} |  | ||||||
|               </paper-input-container> |  | ||||||
|             </label> |  | ||||||
|           `} |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _handleDrop(ev: DragEvent) { |  | ||||||
|     ev.preventDefault(); |  | ||||||
|     ev.stopPropagation(); |  | ||||||
|     if (ev.dataTransfer?.files) { |  | ||||||
|       if (this.crop) { |  | ||||||
|         this._cropFile(ev.dataTransfer.files[0]); |  | ||||||
|       } else { |  | ||||||
|         this._uploadFile(ev.dataTransfer.files[0]); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     this._drag = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _handleDragStart(ev: DragEvent) { |  | ||||||
|     ev.preventDefault(); |  | ||||||
|     ev.stopPropagation(); |  | ||||||
|     this._drag = true; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _handleDragEnd(ev: DragEvent) { |  | ||||||
|     ev.preventDefault(); |  | ||||||
|     ev.stopPropagation(); |  | ||||||
|     this._drag = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _handleFilePicked(ev) { |  | ||||||
|     if (this.crop) { |  | ||||||
|       this._cropFile(ev.target.files[0]); |  | ||||||
|     } else { |  | ||||||
|       this._uploadFile(ev.target.files[0]); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _cropFile(file: File) { |  | ||||||
|     if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) { |  | ||||||
|       this._error = this.hass.localize( |  | ||||||
|         "ui.components.picture-upload.unsupported_format" |  | ||||||
|       ); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     showImageCropperDialog(this, { |  | ||||||
|       file, |  | ||||||
|       options: this.cropOptions || { |  | ||||||
|         round: false, |  | ||||||
|         aspectRatio: NaN, |  | ||||||
|       }, |  | ||||||
|       croppedCallback: (croppedFile) => { |  | ||||||
|         this._uploadFile(croppedFile); |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _uploadFile(file: File) { |  | ||||||
|     if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) { |  | ||||||
|       this._error = this.hass.localize( |  | ||||||
|         "ui.components.picture-upload.unsupported_format" |  | ||||||
|       ); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     this._uploading = true; |  | ||||||
|     this._error = ""; |  | ||||||
|     try { |  | ||||||
|       const media = await createImage(this.hass, file); |  | ||||||
|       this.value = generateImageThumbnailUrl(media.id, this.size); |  | ||||||
|       fireEvent(this, "change"); |  | ||||||
|     } catch (err) { |  | ||||||
|       this._error = err.toString(); |  | ||||||
|     } finally { |  | ||||||
|       this._uploading = false; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _clearPicture(ev: Event) { |  | ||||||
|     ev.preventDefault(); |  | ||||||
|     this.value = null; |  | ||||||
|     this._error = ""; |  | ||||||
|     fireEvent(this, "change"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles() { |  | ||||||
|     return css` |  | ||||||
|       .error { |  | ||||||
|         color: var(--error-color); |  | ||||||
|       } |  | ||||||
|       paper-input-container { |  | ||||||
|         position: relative; |  | ||||||
|         padding: 8px; |  | ||||||
|         margin: 0 -8px; |  | ||||||
|       } |  | ||||||
|       paper-input-container.dragged:before { |  | ||||||
|         position: var(--layout-fit_-_position); |  | ||||||
|         top: var(--layout-fit_-_top); |  | ||||||
|         right: var(--layout-fit_-_right); |  | ||||||
|         bottom: var(--layout-fit_-_bottom); |  | ||||||
|         left: var(--layout-fit_-_left); |  | ||||||
|         background: currentColor; |  | ||||||
|         content: ""; |  | ||||||
|         opacity: var(--dark-divider-opacity); |  | ||||||
|         pointer-events: none; |  | ||||||
|         border-radius: 4px; |  | ||||||
|       } |  | ||||||
|       img { |  | ||||||
|         max-width: 125px; |  | ||||||
|         max-height: 125px; |  | ||||||
|       } |  | ||||||
|       input.file { |  | ||||||
|         display: none; |  | ||||||
|       } |  | ||||||
|       mwc-icon-button { |  | ||||||
|         --mdc-icon-button-size: 24px; |  | ||||||
|         --mdc-icon-size: 20px; |  | ||||||
|       } |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "ha-picture-upload": HaPictureUpload; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -25,7 +25,7 @@ export class HaSettingsRow extends LitElement { | |||||||
|       </style> |       </style> | ||||||
|       <paper-item-body |       <paper-item-body | ||||||
|         ?two-line=${!this.threeLine} |         ?two-line=${!this.threeLine} | ||||||
|         ?three-line=${this.threeLine} |         ?three-line=${!this.threeLine} | ||||||
|       > |       > | ||||||
|         <slot name="heading"></slot> |         <slot name="heading"></slot> | ||||||
|         <div secondary><slot name="description"></slot></div> |         <div secondary><slot name="description"></slot></div> | ||||||
|   | |||||||
| @@ -1,12 +1,9 @@ | |||||||
| import "@material/mwc-button/mwc-button"; |  | ||||||
| import "@material/mwc-icon-button"; | import "@material/mwc-icon-button"; | ||||||
| import { | import { | ||||||
|   mdiBell, |   mdiBell, | ||||||
|   mdiCellphoneCog, |   mdiCellphoneCog, | ||||||
|   mdiClose, |  | ||||||
|   mdiMenu, |  | ||||||
|   mdiMenuOpen, |   mdiMenuOpen, | ||||||
|   mdiPlus, |   mdiMenu, | ||||||
|   mdiViewDashboard, |   mdiViewDashboard, | ||||||
| } from "@mdi/js"; | } from "@mdi/js"; | ||||||
| import "@polymer/paper-item/paper-icon-item"; | import "@polymer/paper-item/paper-icon-item"; | ||||||
| @@ -16,24 +13,20 @@ import "@polymer/paper-listbox/paper-listbox"; | |||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |  | ||||||
|   eventOptions, |   eventOptions, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |   customElement, | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   PropertyValues, |   PropertyValues, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { classMap } from "lit-html/directives/class-map"; | import { classMap } from "lit-html/directives/class-map"; | ||||||
| import { guard } from "lit-html/directives/guard"; |  | ||||||
| import memoizeOne from "memoize-one"; |  | ||||||
| import { LocalStorage } from "../common/decorators/local-storage"; |  | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
| import { computeDomain } from "../common/entity/compute_domain"; | import { computeDomain } from "../common/entity/compute_domain"; | ||||||
| import { navigate } from "../common/navigate"; |  | ||||||
| import { compare } from "../common/string/compare"; | import { compare } from "../common/string/compare"; | ||||||
| import { computeRTL } from "../common/util/compute_rtl"; | import { computeRTL } from "../common/util/compute_rtl"; | ||||||
| import { ActionHandlerDetail } from "../data/lovelace"; | import { getDefaultPanel } from "../data/panel"; | ||||||
| import { | import { | ||||||
|   PersistentNotification, |   PersistentNotification, | ||||||
|   subscribeNotifications, |   subscribeNotifications, | ||||||
| @@ -42,8 +35,6 @@ import { | |||||||
|   ExternalConfig, |   ExternalConfig, | ||||||
|   getExternalConfig, |   getExternalConfig, | ||||||
| } from "../external_app/external_config"; | } from "../external_app/external_config"; | ||||||
| import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; |  | ||||||
| import { haStyleScrollbar } from "../resources/styles"; |  | ||||||
| import type { HomeAssistant, PanelInfo } from "../types"; | import type { HomeAssistant, PanelInfo } from "../types"; | ||||||
| import "./ha-icon"; | import "./ha-icon"; | ||||||
| import "./ha-menu-button"; | import "./ha-menu-button"; | ||||||
| @@ -63,39 +54,11 @@ const SORT_VALUE_URL_PATHS = { | |||||||
|   config: 11, |   config: 11, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const panelSorter = ( | const panelSorter = (a: PanelInfo, b: PanelInfo) => { | ||||||
|   reverseSort: string[], |  | ||||||
|   defaultPanel: string, |  | ||||||
|   a: PanelInfo, |  | ||||||
|   b: PanelInfo |  | ||||||
| ) => { |  | ||||||
|   const indexA = reverseSort.indexOf(a.url_path); |  | ||||||
|   const indexB = reverseSort.indexOf(b.url_path); |  | ||||||
|   if (indexA !== indexB) { |  | ||||||
|     if (indexA < indexB) { |  | ||||||
|       return 1; |  | ||||||
|     } |  | ||||||
|     return -1; |  | ||||||
|   } |  | ||||||
|   return defaultPanelSorter(defaultPanel, a, b); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const defaultPanelSorter = ( |  | ||||||
|   defaultPanel: string, |  | ||||||
|   a: PanelInfo, |  | ||||||
|   b: PanelInfo |  | ||||||
| ) => { |  | ||||||
|   // Put all the Lovelace at the top. |   // Put all the Lovelace at the top. | ||||||
|   const aLovelace = a.component_name === "lovelace"; |   const aLovelace = a.component_name === "lovelace"; | ||||||
|   const bLovelace = b.component_name === "lovelace"; |   const bLovelace = b.component_name === "lovelace"; | ||||||
|  |  | ||||||
|   if (a.url_path === defaultPanel) { |  | ||||||
|     return -1; |  | ||||||
|   } |  | ||||||
|   if (b.url_path === defaultPanel) { |  | ||||||
|     return 1; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (aLovelace && bLovelace) { |   if (aLovelace && bLovelace) { | ||||||
|     return compare(a.title!, b.title!); |     return compare(a.title!, b.title!); | ||||||
|   } |   } | ||||||
| @@ -122,13 +85,8 @@ const defaultPanelSorter = ( | |||||||
|   return compare(a.title!, b.title!); |   return compare(a.title!, b.title!); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const computePanels = memoizeOne( | const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => { | ||||||
|   ( |   const panels = hass.panels; | ||||||
|     panels: HomeAssistant["panels"], |  | ||||||
|     defaultPanel: HomeAssistant["defaultPanel"], |  | ||||||
|     panelsOrder: string[], |  | ||||||
|     hiddenPanels: string[] |  | ||||||
|   ): [PanelInfo[], PanelInfo[]] => { |  | ||||||
|   if (!panels) { |   if (!panels) { | ||||||
|     return [[], []]; |     return [[], []]; | ||||||
|   } |   } | ||||||
| @@ -137,10 +95,7 @@ const computePanels = memoizeOne( | |||||||
|   const afterSpacer: PanelInfo[] = []; |   const afterSpacer: PanelInfo[] = []; | ||||||
|  |  | ||||||
|   Object.values(panels).forEach((panel) => { |   Object.values(panels).forEach((panel) => { | ||||||
|       if ( |     if (!panel.title || panel.url_path === hass.defaultPanel) { | ||||||
|         hiddenPanels.includes(panel.url_path) || |  | ||||||
|         (!panel.title && panel.url_path !== defaultPanel) |  | ||||||
|       ) { |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     (SHOW_AFTER_SPACER.includes(panel.url_path) |     (SHOW_AFTER_SPACER.includes(panel.url_path) | ||||||
| @@ -149,18 +104,11 @@ const computePanels = memoizeOne( | |||||||
|     ).push(panel); |     ).push(panel); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|     const reverseSort = [...panelsOrder].reverse(); |   beforeSpacer.sort(panelSorter); | ||||||
|  |   afterSpacer.sort(panelSorter); | ||||||
|     beforeSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b)); |  | ||||||
|     afterSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b)); |  | ||||||
|  |  | ||||||
|   return [beforeSpacer, afterSpacer]; |   return [beforeSpacer, afterSpacer]; | ||||||
|   } | }; | ||||||
| ); |  | ||||||
|  |  | ||||||
| let Sortable; |  | ||||||
|  |  | ||||||
| let sortStyles: CSSResult; |  | ||||||
|  |  | ||||||
| @customElement("ha-sidebar") | @customElement("ha-sidebar") | ||||||
| class HaSidebar extends LitElement { | class HaSidebar extends LitElement { | ||||||
| @@ -176,34 +124,16 @@ class HaSidebar extends LitElement { | |||||||
|  |  | ||||||
|   @internalProperty() private _notifications?: PersistentNotification[]; |   @internalProperty() private _notifications?: PersistentNotification[]; | ||||||
|  |  | ||||||
|   @internalProperty() private _editMode = false; |  | ||||||
|  |  | ||||||
|   // property used only in css |   // property used only in css | ||||||
|   // @ts-ignore |   // @ts-ignore | ||||||
|   @property({ type: Boolean, reflect: true }) public rtl = false; |   @property({ type: Boolean, reflect: true }) public rtl = false; | ||||||
|  |  | ||||||
|   @internalProperty() private _renderEmptySortable = false; |  | ||||||
|  |  | ||||||
|   private _mouseLeaveTimeout?: number; |   private _mouseLeaveTimeout?: number; | ||||||
|  |  | ||||||
|   private _tooltipHideTimeout?: number; |   private _tooltipHideTimeout?: number; | ||||||
|  |  | ||||||
|   private _recentKeydownActiveUntil = 0; |   private _recentKeydownActiveUntil = 0; | ||||||
|  |  | ||||||
|   // @ts-ignore |  | ||||||
|   @LocalStorage("sidebarPanelOrder", true, { |  | ||||||
|     attribute: false, |  | ||||||
|   }) |  | ||||||
|   private _panelOrder: string[] = []; |  | ||||||
|  |  | ||||||
|   // @ts-ignore |  | ||||||
|   @LocalStorage("sidebarHiddenPanels", true, { |  | ||||||
|     attribute: false, |  | ||||||
|   }) |  | ||||||
|   private _hiddenPanels: string[] = []; |  | ||||||
|  |  | ||||||
|   private _sortable?; |  | ||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
|     const hass = this.hass; |     const hass = this.hass; | ||||||
|  |  | ||||||
| @@ -211,12 +141,7 @@ class HaSidebar extends LitElement { | |||||||
|       return html``; |       return html``; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const [beforeSpacer, afterSpacer] = computePanels( |     const [beforeSpacer, afterSpacer] = computePanels(hass); | ||||||
|       hass.panels, |  | ||||||
|       hass.defaultPanel, |  | ||||||
|       this._panelOrder, |  | ||||||
|       this._hiddenPanels |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     let notificationCount = this._notifications |     let notificationCount = this._notifications | ||||||
|       ? this._notifications.length |       ? this._notifications.length | ||||||
| @@ -227,14 +152,9 @@ class HaSidebar extends LitElement { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const defaultPanel = getDefaultPanel(hass); | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       ${this._editMode |  | ||||||
|         ? html` |  | ||||||
|             <style> |  | ||||||
|               ${sortStyles?.cssText} |  | ||||||
|             </style> |  | ||||||
|           ` |  | ||||||
|         : ""} |  | ||||||
|       <div class="menu"> |       <div class="menu"> | ||||||
|         ${!this.narrow |         ${!this.narrow | ||||||
|           ? html` |           ? html` | ||||||
| @@ -250,69 +170,40 @@ class HaSidebar extends LitElement { | |||||||
|               </mwc-icon-button> |               </mwc-icon-button> | ||||||
|             ` |             ` | ||||||
|           : ""} |           : ""} | ||||||
|         <div class="title"> |         <span class="title">Home Assistant</span> | ||||||
|           ${this._editMode |  | ||||||
|             ? html`<mwc-button outlined @click=${this._closeEditMode}> |  | ||||||
|                 DONE |  | ||||||
|               </mwc-button>` |  | ||||||
|             : "Home Assistant"} |  | ||||||
|         </div> |  | ||||||
|       </div> |       </div> | ||||||
|       <paper-listbox |       <paper-listbox | ||||||
|         attr-for-selected="data-panel" |         attr-for-selected="data-panel" | ||||||
|         class="ha-scrollbar" |  | ||||||
|         .selected=${hass.panelUrl} |         .selected=${hass.panelUrl} | ||||||
|         @focusin=${this._listboxFocusIn} |         @focusin=${this._listboxFocusIn} | ||||||
|         @focusout=${this._listboxFocusOut} |         @focusout=${this._listboxFocusOut} | ||||||
|         @scroll=${this._listboxScroll} |         @scroll=${this._listboxScroll} | ||||||
|         @keydown=${this._listboxKeydown} |         @keydown=${this._listboxKeydown} | ||||||
|         @action=${this._handleAction} |  | ||||||
|         .actionHandler=${actionHandler({ |  | ||||||
|           hasHold: !this._editMode, |  | ||||||
|           disabled: this._editMode, |  | ||||||
|         })} |  | ||||||
|       > |       > | ||||||
|         ${this._editMode |         ${this._renderPanel( | ||||||
|           ? html`<div id="sortable"> |           defaultPanel.url_path, | ||||||
|               ${guard([this._hiddenPanels, this._renderEmptySortable], () => |           defaultPanel.title || hass.localize("panel.states"), | ||||||
|                 this._renderEmptySortable |           defaultPanel.icon, | ||||||
|                   ? "" |           !defaultPanel.icon ? mdiViewDashboard : undefined | ||||||
|                   : this._renderPanels(beforeSpacer) |         )} | ||||||
|  |         ${beforeSpacer.map((panel) => | ||||||
|  |           this._renderPanel( | ||||||
|  |             panel.url_path, | ||||||
|  |             hass.localize(`panel.${panel.title}`) || panel.title, | ||||||
|  |             panel.icon, | ||||||
|  |             undefined | ||||||
|  |           ) | ||||||
|         )} |         )} | ||||||
|             </div>` |  | ||||||
|           : this._renderPanels(beforeSpacer)} |  | ||||||
|         <div class="spacer" disabled></div> |         <div class="spacer" disabled></div> | ||||||
|         ${this._editMode && this._hiddenPanels.length |  | ||||||
|           ? html` |         ${afterSpacer.map((panel) => | ||||||
|               ${this._hiddenPanels.map((url) => { |           this._renderPanel( | ||||||
|                 const panel = this.hass.panels[url]; |             panel.url_path, | ||||||
|                 return html`<paper-icon-item |             hass.localize(`panel.${panel.title}`) || panel.title, | ||||||
|                   @click=${this._unhidePanel} |             panel.icon, | ||||||
|                   class="hidden-panel" |             undefined | ||||||
|                 > |           ) | ||||||
|                   <ha-icon |         )} | ||||||
|                     slot="item-icon" |  | ||||||
|                     .icon=${panel.url_path === "lovelace" |  | ||||||
|                       ? "mdi:view-dashboard" |  | ||||||
|                       : panel.icon} |  | ||||||
|                   ></ha-icon> |  | ||||||
|                   <span class="item-text" |  | ||||||
|                     >${panel.url_path === "lovelace" |  | ||||||
|                       ? hass.localize("panel.states") |  | ||||||
|                       : hass.localize(`panel.${panel.title}`) || |  | ||||||
|                         panel.title}</span |  | ||||||
|                   > |  | ||||||
|                   <ha-svg-icon |  | ||||||
|                     class="hide-panel" |  | ||||||
|                     .panel=${url} |  | ||||||
|                     .path=${mdiPlus} |  | ||||||
|                   ></ha-svg-icon> |  | ||||||
|                 </paper-icon-item>`; |  | ||||||
|               })} |  | ||||||
|               <div class="spacer" disabled></div> |  | ||||||
|             ` |  | ||||||
|           : ""} |  | ||||||
|         ${this._renderPanels(afterSpacer)} |  | ||||||
|         ${this._externalConfig && this._externalConfig.hasSettingsScreen |         ${this._externalConfig && this._externalConfig.hasSettingsScreen | ||||||
|           ? html` |           ? html` | ||||||
|               <a |               <a | ||||||
| @@ -322,7 +213,7 @@ class HaSidebar extends LitElement { | |||||||
|                 )} |                 )} | ||||||
|                 href="#external-app-configuration" |                 href="#external-app-configuration" | ||||||
|                 tabindex="-1" |                 tabindex="-1" | ||||||
|                 @panel-tap=${this._handleExternalAppConfiguration} |                 @click=${this._handleExternalAppConfiguration} | ||||||
|                 @mouseenter=${this._itemMouseEnter} |                 @mouseenter=${this._itemMouseEnter} | ||||||
|                 @mouseleave=${this._itemMouseLeave} |                 @mouseleave=${this._itemMouseLeave} | ||||||
|               > |               > | ||||||
| @@ -386,11 +277,7 @@ class HaSidebar extends LitElement { | |||||||
|         @mouseleave=${this._itemMouseLeave} |         @mouseleave=${this._itemMouseLeave} | ||||||
|       > |       > | ||||||
|         <paper-icon-item> |         <paper-icon-item> | ||||||
|           <ha-user-badge |           <ha-user-badge slot="item-icon" .user=${hass.user}></ha-user-badge> | ||||||
|             slot="item-icon" |  | ||||||
|             .user=${hass.user} |  | ||||||
|             .hass=${hass} |  | ||||||
|           ></ha-user-badge> |  | ||||||
|  |  | ||||||
|           <span class="item-text"> |           <span class="item-text"> | ||||||
|             ${hass.user ? hass.user.name : ""} |             ${hass.user ? hass.user.name : ""} | ||||||
| @@ -408,11 +295,7 @@ class HaSidebar extends LitElement { | |||||||
|       changedProps.has("narrow") || |       changedProps.has("narrow") || | ||||||
|       changedProps.has("alwaysExpand") || |       changedProps.has("alwaysExpand") || | ||||||
|       changedProps.has("_externalConfig") || |       changedProps.has("_externalConfig") || | ||||||
|       changedProps.has("_notifications") || |       changedProps.has("_notifications") | ||||||
|       changedProps.has("_editMode") || |  | ||||||
|       changedProps.has("_renderEmptySortable") || |  | ||||||
|       changedProps.has("_hiddenPanels") || |  | ||||||
|       (changedProps.has("_panelOrder") && !this._editMode) |  | ||||||
|     ) { |     ) { | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
| @@ -478,79 +361,6 @@ class HaSidebar extends LitElement { | |||||||
|     return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement; |     return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _handleAction(ev: CustomEvent<ActionHandlerDetail>) { |  | ||||||
|     if (ev.detail.action === "tap") { |  | ||||||
|       fireEvent(ev.target as HTMLElement, "panel-tap"); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!Sortable) { |  | ||||||
|       const [sortableImport, sortStylesImport] = await Promise.all([ |  | ||||||
|         import("sortablejs/modular/sortable.core.esm"), |  | ||||||
|         import("../resources/ha-sortable-style"), |  | ||||||
|       ]); |  | ||||||
|  |  | ||||||
|       sortStyles = sortStylesImport.sortableStyles; |  | ||||||
|  |  | ||||||
|       Sortable = sortableImport.Sortable; |  | ||||||
|       Sortable.mount(sortableImport.OnSpill); |  | ||||||
|       Sortable.mount(sortableImport.AutoScroll()); |  | ||||||
|     } |  | ||||||
|     this._editMode = true; |  | ||||||
|  |  | ||||||
|     if (!this.expanded) { |  | ||||||
|       fireEvent(this, "hass-toggle-menu"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     await this.updateComplete; |  | ||||||
|  |  | ||||||
|     this._createSortable(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _createSortable() { |  | ||||||
|     this._sortable = new Sortable(this.shadowRoot!.getElementById("sortable"), { |  | ||||||
|       animation: 150, |  | ||||||
|       fallbackClass: "sortable-fallback", |  | ||||||
|       dataIdAttr: "data-panel", |  | ||||||
|       onSort: async () => { |  | ||||||
|         this._panelOrder = this._sortable.toArray(); |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _closeEditMode() { |  | ||||||
|     this._sortable?.destroy(); |  | ||||||
|     this._sortable = undefined; |  | ||||||
|     this._editMode = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _hidePanel(ev: Event) { |  | ||||||
|     ev.preventDefault(); |  | ||||||
|     const panel = (ev.currentTarget as any).panel; |  | ||||||
|     if (this._hiddenPanels.includes(panel)) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     // Make a copy for Memoize |  | ||||||
|     this._hiddenPanels = [...this._hiddenPanels, panel]; |  | ||||||
|     this._renderEmptySortable = true; |  | ||||||
|     await this.updateComplete; |  | ||||||
|     this._renderEmptySortable = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _unhidePanel(ev: Event) { |  | ||||||
|     ev.preventDefault(); |  | ||||||
|     const index = this._hiddenPanels.indexOf((ev.target as any).panel); |  | ||||||
|     if (index < 0) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     this._hiddenPanels.splice(index, 1); |  | ||||||
|     // Make a copy for Memoize |  | ||||||
|     this._hiddenPanels = [...this._hiddenPanels]; |  | ||||||
|     this._renderEmptySortable = true; |  | ||||||
|     await this.updateComplete; |  | ||||||
|     this._renderEmptySortable = false; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _itemMouseEnter(ev: MouseEvent) { |   private _itemMouseEnter(ev: MouseEvent) { | ||||||
|     // On keypresses on the listbox, we're going to ignore mouse enter events |     // On keypresses on the listbox, we're going to ignore mouse enter events | ||||||
|     // for 100ms so that we ignore it when pressing down arrow scrolls the |     // for 100ms so that we ignore it when pressing down arrow scrolls the | ||||||
| @@ -647,26 +457,6 @@ class HaSidebar extends LitElement { | |||||||
|     fireEvent(this, "hass-toggle-menu"); |     fireEvent(this, "hass-toggle-menu"); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _renderPanels(panels: PanelInfo[]) { |  | ||||||
|     return panels.map((panel) => |  | ||||||
|       this._renderPanel( |  | ||||||
|         panel.url_path, |  | ||||||
|         panel.url_path === "lovelace" |  | ||||||
|           ? this.hass.localize("panel.states") |  | ||||||
|           : this.hass.localize(`panel.${panel.title}`) || panel.title, |  | ||||||
|         panel.url_path === "lovelace" ? undefined : panel.icon, |  | ||||||
|         panel.url_path === "lovelace" ? mdiViewDashboard : undefined |  | ||||||
|       ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _handlePanelTap(ev: Event) { |  | ||||||
|     const path = __DEMO__ |  | ||||||
|       ? (ev.currentTarget as HTMLAnchorElement).getAttribute("href")! |  | ||||||
|       : (ev.currentTarget as HTMLAnchorElement).href; |  | ||||||
|     navigate(this, path); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _renderPanel( |   private _renderPanel( | ||||||
|     urlPath: string, |     urlPath: string, | ||||||
|     title: string | null, |     title: string | null, | ||||||
| @@ -676,10 +466,9 @@ class HaSidebar extends LitElement { | |||||||
|     return html` |     return html` | ||||||
|       <a |       <a | ||||||
|         aria-role="option" |         aria-role="option" | ||||||
|         href=${`/${urlPath}`} |         href="${`/${urlPath}`}" | ||||||
|         data-panel=${urlPath} |         data-panel="${urlPath}" | ||||||
|         tabindex="-1" |         tabindex="-1" | ||||||
|         @panel-tap=${this._handlePanelTap} |  | ||||||
|         @mouseenter=${this._itemMouseEnter} |         @mouseenter=${this._itemMouseEnter} | ||||||
|         @mouseleave=${this._itemMouseLeave} |         @mouseleave=${this._itemMouseLeave} | ||||||
|       > |       > | ||||||
| @@ -691,24 +480,13 @@ class HaSidebar extends LitElement { | |||||||
|               ></ha-svg-icon>` |               ></ha-svg-icon>` | ||||||
|             : html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`} |             : html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`} | ||||||
|           <span class="item-text">${title}</span> |           <span class="item-text">${title}</span> | ||||||
|           ${this._editMode |  | ||||||
|             ? html`<mwc-icon-button |  | ||||||
|                 class="hide-panel" |  | ||||||
|                 .panel=${urlPath} |  | ||||||
|                 @click=${this._hidePanel} |  | ||||||
|               > |  | ||||||
|                 <ha-svg-icon .path=${mdiClose}></ha-svg-icon> |  | ||||||
|               </mwc-icon-button>` |  | ||||||
|             : ""} |  | ||||||
|         </paper-icon-item> |         </paper-icon-item> | ||||||
|       </a> |       </a> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResult[] { |   static get styles(): CSSResult { | ||||||
|     return [ |     return css` | ||||||
|       haStyleScrollbar, |  | ||||||
|       css` |  | ||||||
|       :host { |       :host { | ||||||
|         height: 100%; |         height: 100%; | ||||||
|         display: block; |         display: block; | ||||||
| @@ -764,14 +542,21 @@ class HaSidebar extends LitElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       .title { |       .title { | ||||||
|           width: 100%; |  | ||||||
|         display: none; |         display: none; | ||||||
|       } |       } | ||||||
|       :host([expanded]) .title { |       :host([expanded]) .title { | ||||||
|         display: initial; |         display: initial; | ||||||
|       } |       } | ||||||
|         .title mwc-button { |  | ||||||
|           width: 100%; |       paper-listbox::-webkit-scrollbar { | ||||||
|  |         width: 0.4rem; | ||||||
|  |         height: 0.4rem; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       paper-listbox::-webkit-scrollbar-thumb { | ||||||
|  |         -webkit-border-radius: 4px; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         background: var(--scrollbar-thumb-color); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       paper-listbox { |       paper-listbox { | ||||||
| @@ -780,7 +565,10 @@ class HaSidebar extends LitElement { | |||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|         box-sizing: border-box; |         box-sizing: border-box; | ||||||
|         height: calc(100% - 196px - env(safe-area-inset-bottom)); |         height: calc(100% - 196px - env(safe-area-inset-bottom)); | ||||||
|  |         overflow-y: auto; | ||||||
|         overflow-x: hidden; |         overflow-x: hidden; | ||||||
|  |         scrollbar-color: var(--scrollbar-thumb-color) transparent; | ||||||
|  |         scrollbar-width: thin; | ||||||
|         background: none; |         background: none; | ||||||
|         margin-left: env(safe-area-inset-left); |         margin-left: env(safe-area-inset-left); | ||||||
|       } |       } | ||||||
| @@ -980,8 +768,7 @@ class HaSidebar extends LitElement { | |||||||
|         -webkit-transform: scaleX(-1); |         -webkit-transform: scaleX(-1); | ||||||
|         transform: scaleX(-1); |         transform: scaleX(-1); | ||||||
|       } |       } | ||||||
|       `, |     `; | ||||||
|     ]; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -989,8 +776,4 @@ declare global { | |||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
|     "ha-sidebar": HaSidebar; |     "ha-sidebar": HaSidebar; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   interface HASSDomEvents { |  | ||||||
|     "panel-tap": undefined; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -21,8 +21,8 @@ import { | |||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import { | import { | ||||||
|   LeafletModuleType, |   LeafletModuleType, | ||||||
|   replaceTileLayer, |  | ||||||
|   setupLeafletMap, |   setupLeafletMap, | ||||||
|  |   replaceTileLayer, | ||||||
| } from "../../common/dom/setup-leaflet-map"; | } from "../../common/dom/setup-leaflet-map"; | ||||||
| import { nextRender } from "../../common/util/render-status"; | import { nextRender } from "../../common/util/render-status"; | ||||||
| import { defaultRadiusColor } from "../../data/zone"; | import { defaultRadiusColor } from "../../data/zone"; | ||||||
| @@ -40,8 +40,6 @@ class LocationEditor extends LitElement { | |||||||
|  |  | ||||||
|   @property() public icon?: string; |   @property() public icon?: string; | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public darkMode?: boolean; |  | ||||||
|  |  | ||||||
|   public fitZoom = 16; |   public fitZoom = 16; | ||||||
|  |  | ||||||
|   private _iconEl?: DivIcon; |   private _iconEl?: DivIcon; | ||||||
| @@ -131,7 +129,7 @@ class LocationEditor extends LitElement { | |||||||
|   private async _initMap(): Promise<void> { |   private async _initMap(): Promise<void> { | ||||||
|     [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( |     [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( | ||||||
|       this._mapEl, |       this._mapEl, | ||||||
|       this.darkMode ?? this.hass.themes?.darkMode, |       this.hass.themes?.darkMode, | ||||||
|       Boolean(this.radius) |       Boolean(this.radius) | ||||||
|     ); |     ); | ||||||
|     this._leafletMap.addEventListener( |     this._leafletMap.addEventListener( | ||||||
|   | |||||||
| @@ -1,113 +0,0 @@ | |||||||
| import { |  | ||||||
|   css, |  | ||||||
|   CSSResultArray, |  | ||||||
|   customElement, |  | ||||||
|   html, |  | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |  | ||||||
|   property, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit-element"; |  | ||||||
| import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event"; |  | ||||||
| import type { |  | ||||||
|   MediaPickedEvent, |  | ||||||
|   MediaPlayerBrowseAction, |  | ||||||
| } from "../../data/media-player"; |  | ||||||
| import { haStyleDialog } from "../../resources/styles"; |  | ||||||
| import type { HomeAssistant } from "../../types"; |  | ||||||
| import "../ha-dialog"; |  | ||||||
| import "./ha-media-player-browse"; |  | ||||||
| import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog"; |  | ||||||
|  |  | ||||||
| @customElement("dialog-media-player-browse") |  | ||||||
| class DialogMediaPlayerBrowse extends LitElement { |  | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _entityId!: string; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _mediaContentId?: string; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _mediaContentType?: string; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _action?: MediaPlayerBrowseAction; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _params?: MediaPlayerBrowseDialogParams; |  | ||||||
|  |  | ||||||
|   public showDialog(params: MediaPlayerBrowseDialogParams): void { |  | ||||||
|     this._params = params; |  | ||||||
|     this._entityId = this._params.entityId; |  | ||||||
|     this._mediaContentId = this._params.mediaContentId; |  | ||||||
|     this._mediaContentType = this._params.mediaContentType; |  | ||||||
|     this._action = this._params.action || "play"; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public closeDialog() { |  | ||||||
|     this._params = undefined; |  | ||||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |  | ||||||
|     if (!this._params) { |  | ||||||
|       return html``; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return html` |  | ||||||
|       <ha-dialog |  | ||||||
|         open |  | ||||||
|         scrimClickAction |  | ||||||
|         escapeKeyAction |  | ||||||
|         hideActions |  | ||||||
|         flexContent |  | ||||||
|         @closed=${this.closeDialog} |  | ||||||
|       > |  | ||||||
|         <ha-media-player-browse |  | ||||||
|           dialog |  | ||||||
|           .hass=${this.hass} |  | ||||||
|           .entityId=${this._entityId} |  | ||||||
|           .action=${this._action!} |  | ||||||
|           .mediaContentId=${this._mediaContentId} |  | ||||||
|           .mediaContentType=${this._mediaContentType} |  | ||||||
|           @close-dialog=${this.closeDialog} |  | ||||||
|           @media-picked=${this._mediaPicked} |  | ||||||
|         ></ha-media-player-browse> |  | ||||||
|       </ha-dialog> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _mediaPicked(ev: HASSDomEvent<MediaPickedEvent>): void { |  | ||||||
|     this._params!.mediaPickedCallback(ev.detail); |  | ||||||
|     if (this._action !== "play") { |  | ||||||
|       this.closeDialog(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResultArray { |  | ||||||
|     return [ |  | ||||||
|       haStyleDialog, |  | ||||||
|       css` |  | ||||||
|         ha-dialog { |  | ||||||
|           --dialog-z-index: 8; |  | ||||||
|           --dialog-content-padding: 0; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         @media (min-width: 800px) { |  | ||||||
|           ha-dialog { |  | ||||||
|             --mdc-dialog-max-width: 800px; |  | ||||||
|             --dialog-surface-position: fixed; |  | ||||||
|             --dialog-surface-top: 40px; |  | ||||||
|             --mdc-dialog-max-height: calc(100% - 72px); |  | ||||||
|           } |  | ||||||
|           ha-media-player-browse { |  | ||||||
|             width: 700px; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       `, |  | ||||||
|     ]; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "dialog-media-player-browse": DialogMediaPlayerBrowse; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,873 +0,0 @@ | |||||||
| import "@material/mwc-button/mwc-button"; |  | ||||||
| import "@material/mwc-fab/mwc-fab"; |  | ||||||
| import "@material/mwc-list/mwc-list"; |  | ||||||
| import "@material/mwc-list/mwc-list-item"; |  | ||||||
| import { mdiArrowLeft, mdiClose, mdiFolder, mdiPlay, mdiPlus } from "@mdi/js"; |  | ||||||
| import "@polymer/paper-item/paper-item"; |  | ||||||
| import "@polymer/paper-listbox/paper-listbox"; |  | ||||||
| import { |  | ||||||
|   css, |  | ||||||
|   CSSResultArray, |  | ||||||
|   customElement, |  | ||||||
|   html, |  | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |  | ||||||
|   property, |  | ||||||
|   PropertyValues, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit-element"; |  | ||||||
| import { classMap } from "lit-html/directives/class-map"; |  | ||||||
| import { ifDefined } from "lit-html/directives/if-defined"; |  | ||||||
| import { styleMap } from "lit-html/directives/style-map"; |  | ||||||
| import memoizeOne from "memoize-one"; |  | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; |  | ||||||
| import { computeRTLDirection } from "../../common/util/compute_rtl"; |  | ||||||
| import { debounce } from "../../common/util/debounce"; |  | ||||||
| import { |  | ||||||
|   browseLocalMediaPlayer, |  | ||||||
|   browseMediaPlayer, |  | ||||||
|   BROWSER_SOURCE, |  | ||||||
|   MediaPickedEvent, |  | ||||||
|   MediaPlayerBrowseAction, |  | ||||||
| } from "../../data/media-player"; |  | ||||||
| import type { MediaPlayerItem } from "../../data/media-player"; |  | ||||||
| import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; |  | ||||||
| import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer"; |  | ||||||
| import { haStyle } from "../../resources/styles"; |  | ||||||
| import type { HomeAssistant } from "../../types"; |  | ||||||
| import "../entity/ha-entity-picker"; |  | ||||||
| import "../ha-button-menu"; |  | ||||||
| import "../ha-card"; |  | ||||||
| import "../ha-circular-progress"; |  | ||||||
| import "../ha-paper-dropdown-menu"; |  | ||||||
| import "../ha-svg-icon"; |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HASSDomEvents { |  | ||||||
|     "media-picked": MediaPickedEvent; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @customElement("ha-media-player-browse") |  | ||||||
| export class HaMediaPlayerBrowse extends LitElement { |  | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   @property() public entityId!: string; |  | ||||||
|  |  | ||||||
|   @property() public mediaContentId?: string; |  | ||||||
|  |  | ||||||
|   @property() public mediaContentType?: string; |  | ||||||
|  |  | ||||||
|   @property() public action: MediaPlayerBrowseAction = "play"; |  | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) public dialog = false; |  | ||||||
|  |  | ||||||
|   @property({ type: Boolean, attribute: "narrow", reflect: true }) |  | ||||||
|   private _narrow = false; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _loading = false; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _error?: { message: string; code: string }; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _mediaPlayerItems: MediaPlayerItem[] = []; |  | ||||||
|  |  | ||||||
|   private _resizeObserver?: ResizeObserver; |  | ||||||
|  |  | ||||||
|   public connectedCallback(): void { |  | ||||||
|     super.connectedCallback(); |  | ||||||
|     this.updateComplete.then(() => this._attachObserver()); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public disconnectedCallback(): void { |  | ||||||
|     if (this._resizeObserver) { |  | ||||||
|       this._resizeObserver.disconnect(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public navigateBack() { |  | ||||||
|     this._mediaPlayerItems!.pop(); |  | ||||||
|     const item = this._mediaPlayerItems!.pop(); |  | ||||||
|     if (!item) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     this._navigate(item); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _renderError(err: { message: string; code: string }) { |  | ||||||
|     if (err.message === "Media directory does not exist.") { |  | ||||||
|       return html` |  | ||||||
|         <h2>No local media found.</h2> |  | ||||||
|         <p> |  | ||||||
|           It looks like you have not yet created a media directory. |  | ||||||
|           <br />Create a directory with the name <b>"media"</b> in the |  | ||||||
|           configuration directory of Home Assistant |  | ||||||
|           (${this.hass.config.config_dir}). <br />Place your video, audio and |  | ||||||
|           image files in this directory to be able to browse and play them in |  | ||||||
|           the browser or on supported media players. |  | ||||||
|         </p> |  | ||||||
|  |  | ||||||
|         <p> |  | ||||||
|           Check the |  | ||||||
|           <a |  | ||||||
|             href="https://www.home-assistant.io/integrations/media_source/#local-media" |  | ||||||
|             target="_blank" |  | ||||||
|             rel="noreferrer" |  | ||||||
|             >documentation</a |  | ||||||
|           > |  | ||||||
|           for more info |  | ||||||
|         </p> |  | ||||||
|       `; |  | ||||||
|     } |  | ||||||
|     return err.message; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |  | ||||||
|     if (this._loading) { |  | ||||||
|       return html`<ha-circular-progress active></ha-circular-progress>`; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (this._error && !this._mediaPlayerItems.length) { |  | ||||||
|       if (this.dialog) { |  | ||||||
|         this._closeDialogAction(); |  | ||||||
|         showAlertDialog(this, { |  | ||||||
|           title: this.hass.localize( |  | ||||||
|             "ui.components.media-browser.media_browsing_error" |  | ||||||
|           ), |  | ||||||
|           text: this._renderError(this._error), |  | ||||||
|         }); |  | ||||||
|       } else { |  | ||||||
|         return html`<div class="container error"> |  | ||||||
|           ${this._renderError(this._error)} |  | ||||||
|         </div>`; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!this._mediaPlayerItems.length) { |  | ||||||
|       return html``; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const currentItem = this._mediaPlayerItems[ |  | ||||||
|       this._mediaPlayerItems.length - 1 |  | ||||||
|     ]; |  | ||||||
|  |  | ||||||
|     const previousItem: MediaPlayerItem | undefined = |  | ||||||
|       this._mediaPlayerItems.length > 1 |  | ||||||
|         ? this._mediaPlayerItems[this._mediaPlayerItems.length - 2] |  | ||||||
|         : undefined; |  | ||||||
|  |  | ||||||
|     const hasExpandableChildren: |  | ||||||
|       | MediaPlayerItem |  | ||||||
|       | undefined = this._hasExpandableChildren(currentItem.children); |  | ||||||
|  |  | ||||||
|     const showImages: boolean | undefined = currentItem.children?.some( |  | ||||||
|       (child) => child.thumbnail && child.thumbnail !== currentItem.thumbnail |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     const mediaType = this.hass.localize( |  | ||||||
|       `ui.components.media-browser.content-type.${currentItem.media_content_type}` |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     return html` |  | ||||||
|       <div |  | ||||||
|         class="header  ${classMap({ |  | ||||||
|           "no-img": !currentItem.thumbnail, |  | ||||||
|           "no-dialog": !this.dialog, |  | ||||||
|         })}" |  | ||||||
|       > |  | ||||||
|         <div class="header-content"> |  | ||||||
|           ${currentItem.thumbnail |  | ||||||
|             ? html` |  | ||||||
|                 <div |  | ||||||
|                   class="img" |  | ||||||
|                   style=${styleMap({ |  | ||||||
|                     backgroundImage: currentItem.thumbnail |  | ||||||
|                       ? `url(${currentItem.thumbnail})` |  | ||||||
|                       : "none", |  | ||||||
|                   })} |  | ||||||
|                 > |  | ||||||
|                   ${this._narrow && currentItem?.can_play |  | ||||||
|                     ? html` |  | ||||||
|                         <mwc-fab |  | ||||||
|                           mini |  | ||||||
|                           .item=${currentItem} |  | ||||||
|                           @click=${this._actionClicked} |  | ||||||
|                         > |  | ||||||
|                           <ha-svg-icon |  | ||||||
|                             slot="icon" |  | ||||||
|                             .label=${this.hass.localize( |  | ||||||
|                               `ui.components.media-browser.${this.action}-media` |  | ||||||
|                             )} |  | ||||||
|                             .path=${this.action === "play" ? mdiPlay : mdiPlus} |  | ||||||
|                           ></ha-svg-icon> |  | ||||||
|                           ${this.hass.localize( |  | ||||||
|                             `ui.components.media-browser.${this.action}` |  | ||||||
|                           )} |  | ||||||
|                         </mwc-fab> |  | ||||||
|                       ` |  | ||||||
|                     : ""} |  | ||||||
|                 </div> |  | ||||||
|               ` |  | ||||||
|             : html``} |  | ||||||
|           <div class="header-info"> |  | ||||||
|             <div class="breadcrumb"> |  | ||||||
|               ${previousItem |  | ||||||
|                 ? html` |  | ||||||
|                     <div class="previous-title" @click=${this.navigateBack}> |  | ||||||
|                       <ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon> |  | ||||||
|                       ${previousItem.title} |  | ||||||
|                     </div> |  | ||||||
|                   ` |  | ||||||
|                 : ""} |  | ||||||
|               <h1 class="title">${currentItem.title}</h1> |  | ||||||
|               ${mediaType |  | ||||||
|                 ? html` |  | ||||||
|                     <h2 class="subtitle"> |  | ||||||
|                       ${mediaType} |  | ||||||
|                     </h2> |  | ||||||
|                   ` |  | ||||||
|                 : ""} |  | ||||||
|             </div> |  | ||||||
|             ${currentItem.can_play && (!currentItem.thumbnail || !this._narrow) |  | ||||||
|               ? html` |  | ||||||
|                   <mwc-button |  | ||||||
|                     raised |  | ||||||
|                     .item=${currentItem} |  | ||||||
|                     @click=${this._actionClicked} |  | ||||||
|                   > |  | ||||||
|                     <ha-svg-icon |  | ||||||
|                       slot="icon" |  | ||||||
|                       .label=${this.hass.localize( |  | ||||||
|                         `ui.components.media-browser.${this.action}-media` |  | ||||||
|                       )} |  | ||||||
|                       .path=${this.action === "play" ? mdiPlay : mdiPlus} |  | ||||||
|                     ></ha-svg-icon> |  | ||||||
|                     ${this.hass.localize( |  | ||||||
|                       `ui.components.media-browser.${this.action}` |  | ||||||
|                     )} |  | ||||||
|                   </mwc-button> |  | ||||||
|                 ` |  | ||||||
|               : ""} |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|         ${this.dialog |  | ||||||
|           ? html` |  | ||||||
|               <mwc-icon-button |  | ||||||
|                 aria-label=${this.hass.localize("ui.dialogs.generic.close")} |  | ||||||
|                 @click=${this._closeDialogAction} |  | ||||||
|                 class="header_button" |  | ||||||
|                 dir=${computeRTLDirection(this.hass)} |  | ||||||
|               > |  | ||||||
|                 <ha-svg-icon .path=${mdiClose}></ha-svg-icon> |  | ||||||
|               </mwc-icon-button> |  | ||||||
|             ` |  | ||||||
|           : ""} |  | ||||||
|       </div> |  | ||||||
|       ${this._error |  | ||||||
|         ? html`<div class="container error"> |  | ||||||
|             ${this._renderError(this._error)} |  | ||||||
|           </div>` |  | ||||||
|         : currentItem.children?.length |  | ||||||
|         ? hasExpandableChildren |  | ||||||
|           ? html` |  | ||||||
|               <div class="children"> |  | ||||||
|                 ${currentItem.children.map( |  | ||||||
|                   (child) => html` |  | ||||||
|                     <div |  | ||||||
|                       class="child" |  | ||||||
|                       .item=${child} |  | ||||||
|                       @click=${this._childClicked} |  | ||||||
|                     > |  | ||||||
|                       <div class="ha-card-parent"> |  | ||||||
|                         <ha-card |  | ||||||
|                           outlined |  | ||||||
|                           style=${styleMap({ |  | ||||||
|                             backgroundImage: child.thumbnail |  | ||||||
|                               ? `url(${child.thumbnail})` |  | ||||||
|                               : "none", |  | ||||||
|                           })} |  | ||||||
|                         > |  | ||||||
|                           ${child.can_expand && !child.thumbnail |  | ||||||
|                             ? html` |  | ||||||
|                                 <ha-svg-icon |  | ||||||
|                                   class="folder" |  | ||||||
|                                   .path=${mdiFolder} |  | ||||||
|                                 ></ha-svg-icon> |  | ||||||
|                               ` |  | ||||||
|                             : ""} |  | ||||||
|                         </ha-card> |  | ||||||
|                         ${child.can_play |  | ||||||
|                           ? html` |  | ||||||
|                               <mwc-icon-button |  | ||||||
|                                 class="play" |  | ||||||
|                                 .item=${child} |  | ||||||
|                                 .label=${this.hass.localize( |  | ||||||
|                                   `ui.components.media-browser.${this.action}-media` |  | ||||||
|                                 )} |  | ||||||
|                                 @click=${this._actionClicked} |  | ||||||
|                               > |  | ||||||
|                                 <ha-svg-icon |  | ||||||
|                                   .path=${this.action === "play" |  | ||||||
|                                     ? mdiPlay |  | ||||||
|                                     : mdiPlus} |  | ||||||
|                                 ></ha-svg-icon> |  | ||||||
|                               </mwc-icon-button> |  | ||||||
|                             ` |  | ||||||
|                           : ""} |  | ||||||
|                       </div> |  | ||||||
|                       <div class="title">${child.title}</div> |  | ||||||
|                       <div class="type"> |  | ||||||
|                         ${this.hass.localize( |  | ||||||
|                           `ui.components.media-browser.content-type.${child.media_content_type}` |  | ||||||
|                         )} |  | ||||||
|                       </div> |  | ||||||
|                     </div> |  | ||||||
|                   ` |  | ||||||
|                 )} |  | ||||||
|               </div> |  | ||||||
|             ` |  | ||||||
|           : html` |  | ||||||
|               <mwc-list> |  | ||||||
|                 ${currentItem.children.map( |  | ||||||
|                   (child) => html` |  | ||||||
|                     <mwc-list-item |  | ||||||
|                       @click=${this._actionClicked} |  | ||||||
|                       .item=${child} |  | ||||||
|                       graphic="avatar" |  | ||||||
|                       hasMeta |  | ||||||
|                       dir=${computeRTLDirection(this.hass)} |  | ||||||
|                     > |  | ||||||
|                       <div |  | ||||||
|                         class="graphic" |  | ||||||
|                         style=${ifDefined( |  | ||||||
|                           showImages && child.thumbnail |  | ||||||
|                             ? `background-image: url(${child.thumbnail})` |  | ||||||
|                             : undefined |  | ||||||
|                         )} |  | ||||||
|                         slot="graphic" |  | ||||||
|                       > |  | ||||||
|                         <mwc-icon-button |  | ||||||
|                           class="play ${classMap({ |  | ||||||
|                             show: !showImages || !child.thumbnail, |  | ||||||
|                           })}" |  | ||||||
|                           .item=${child} |  | ||||||
|                           .label=${this.hass.localize( |  | ||||||
|                             `ui.components.media-browser.${this.action}-media` |  | ||||||
|                           )} |  | ||||||
|                           @click=${this._actionClicked} |  | ||||||
|                         > |  | ||||||
|                           <ha-svg-icon |  | ||||||
|                             .path=${this.action === "play" ? mdiPlay : mdiPlus} |  | ||||||
|                           ></ha-svg-icon> |  | ||||||
|                         </mwc-icon-button> |  | ||||||
|                       </div> |  | ||||||
|                       <span class="title">${child.title}</span> |  | ||||||
|                     </mwc-list-item> |  | ||||||
|                     <li divider role="separator"></li> |  | ||||||
|                   ` |  | ||||||
|                 )} |  | ||||||
|               </mwc-list> |  | ||||||
|             ` |  | ||||||
|         : html`<div class="container"> |  | ||||||
|             ${this.hass.localize("ui.components.media-browser.no_items")} |  | ||||||
|           </div>`} |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected firstUpdated(): void { |  | ||||||
|     this._measureCard(); |  | ||||||
|     this._attachObserver(); |  | ||||||
|  |  | ||||||
|     this.addEventListener("scroll", this._scroll, { passive: true }); |  | ||||||
|     this.addEventListener("touchmove", this._scroll, { |  | ||||||
|       passive: true, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected updated(changedProps: PropertyValues): void { |  | ||||||
|     super.updated(changedProps); |  | ||||||
|  |  | ||||||
|     if ( |  | ||||||
|       !changedProps.has("entityId") && |  | ||||||
|       !changedProps.has("mediaContentId") && |  | ||||||
|       !changedProps.has("mediaContentType") && |  | ||||||
|       !changedProps.has("action") |  | ||||||
|     ) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (changedProps.has("entityId")) { |  | ||||||
|       this._error = undefined; |  | ||||||
|       this._mediaPlayerItems = []; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this._fetchData(this.mediaContentId, this.mediaContentType) |  | ||||||
|       .then((itemData) => { |  | ||||||
|         if (!itemData) { |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         this._mediaPlayerItems = [itemData]; |  | ||||||
|       }) |  | ||||||
|       .catch((err) => { |  | ||||||
|         this._error = err; |  | ||||||
|       }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _actionClicked(ev: MouseEvent): void { |  | ||||||
|     ev.stopPropagation(); |  | ||||||
|     const item = (ev.currentTarget as any).item; |  | ||||||
|  |  | ||||||
|     this._runAction(item); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _runAction(item: MediaPlayerItem): void { |  | ||||||
|     fireEvent(this, "media-picked", { item }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _childClicked(ev: MouseEvent): Promise<void> { |  | ||||||
|     const target = ev.currentTarget as any; |  | ||||||
|     const item: MediaPlayerItem = target.item; |  | ||||||
|  |  | ||||||
|     if (!item) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!item.can_expand) { |  | ||||||
|       this._runAction(item); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this._navigate(item); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _navigate(item: MediaPlayerItem) { |  | ||||||
|     this._error = undefined; |  | ||||||
|  |  | ||||||
|     let itemData: MediaPlayerItem; |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       itemData = await this._fetchData( |  | ||||||
|         item.media_content_id, |  | ||||||
|         item.media_content_type |  | ||||||
|       ); |  | ||||||
|     } catch (err) { |  | ||||||
|       showAlertDialog(this, { |  | ||||||
|         title: this.hass.localize( |  | ||||||
|           "ui.components.media-browser.media_browsing_error" |  | ||||||
|         ), |  | ||||||
|         text: this._renderError(err), |  | ||||||
|       }); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.scrollTo(0, 0); |  | ||||||
|     this._mediaPlayerItems = [...this._mediaPlayerItems, itemData]; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _fetchData( |  | ||||||
|     mediaContentId?: string, |  | ||||||
|     mediaContentType?: string |  | ||||||
|   ): Promise<MediaPlayerItem> { |  | ||||||
|     const itemData = |  | ||||||
|       this.entityId !== BROWSER_SOURCE |  | ||||||
|         ? await browseMediaPlayer( |  | ||||||
|             this.hass, |  | ||||||
|             this.entityId, |  | ||||||
|             mediaContentId, |  | ||||||
|             mediaContentType |  | ||||||
|           ) |  | ||||||
|         : await browseLocalMediaPlayer(this.hass, mediaContentId); |  | ||||||
|  |  | ||||||
|     return itemData; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _measureCard(): void { |  | ||||||
|     this._narrow = (this.dialog ? window.innerWidth : this.offsetWidth) < 450; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _scroll(): void { |  | ||||||
|     if (this.scrollTop > (this._narrow ? 224 : 125)) { |  | ||||||
|       this.setAttribute("scroll", ""); |  | ||||||
|     } else if (this.scrollTop === 0) { |  | ||||||
|       this.removeAttribute("scroll"); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _attachObserver(): Promise<void> { |  | ||||||
|     if (!this._resizeObserver) { |  | ||||||
|       await installResizeObserver(); |  | ||||||
|       this._resizeObserver = new ResizeObserver( |  | ||||||
|         debounce(() => this._measureCard(), 250, false) |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this._resizeObserver.observe(this); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _hasExpandableChildren = memoizeOne((children?: MediaPlayerItem[]) => |  | ||||||
|     children?.find((item: MediaPlayerItem) => item.can_expand) |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   private _closeDialogAction(): void { |  | ||||||
|     fireEvent(this, "close-dialog"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResultArray { |  | ||||||
|     return [ |  | ||||||
|       haStyle, |  | ||||||
|       css` |  | ||||||
|         :host { |  | ||||||
|           display: block; |  | ||||||
|           overflow-y: auto; |  | ||||||
|           display: flex; |  | ||||||
|           padding: 0px 0px 20px; |  | ||||||
|           flex-direction: column; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .container { |  | ||||||
|           padding: 16px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .header { |  | ||||||
|           display: flex; |  | ||||||
|           justify-content: space-between; |  | ||||||
|           border-bottom: 1px solid var(--divider-color); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .header { |  | ||||||
|           background-color: var(--card-background-color); |  | ||||||
|           position: sticky; |  | ||||||
|           position: -webkit-sticky; |  | ||||||
|           top: 0; |  | ||||||
|           z-index: 5; |  | ||||||
|           padding: 20px 24px 10px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .header-content { |  | ||||||
|           display: flex; |  | ||||||
|           flex-wrap: wrap; |  | ||||||
|           flex-grow: 1; |  | ||||||
|           align-items: flex-start; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .header-content .img { |  | ||||||
|           height: 200px; |  | ||||||
|           width: 200px; |  | ||||||
|           margin-right: 16px; |  | ||||||
|           background-size: cover; |  | ||||||
|           border-radius: 4px; |  | ||||||
|           transition: width 0.4s, height 0.4s; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .header-info { |  | ||||||
|           display: flex; |  | ||||||
|           flex-direction: column; |  | ||||||
|           justify-content: space-between; |  | ||||||
|           align-self: stretch; |  | ||||||
|           min-width: 0; |  | ||||||
|           flex: 1; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .header-info mwc-button { |  | ||||||
|           display: block; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .breadcrumb { |  | ||||||
|           display: flex; |  | ||||||
|           flex-direction: column; |  | ||||||
|           overflow: hidden; |  | ||||||
|           flex-grow: 1; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .breadcrumb .title { |  | ||||||
|           font-size: 32px; |  | ||||||
|           line-height: 1.2; |  | ||||||
|           font-weight: bold; |  | ||||||
|           margin: 0; |  | ||||||
|           overflow: hidden; |  | ||||||
|           display: -webkit-box; |  | ||||||
|           -webkit-box-orient: vertical; |  | ||||||
|           -webkit-line-clamp: 2; |  | ||||||
|           padding-right: 8px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .breadcrumb .previous-title { |  | ||||||
|           font-size: 14px; |  | ||||||
|           padding-bottom: 8px; |  | ||||||
|           color: var(--secondary-text-color); |  | ||||||
|           overflow: hidden; |  | ||||||
|           text-overflow: ellipsis; |  | ||||||
|           cursor: pointer; |  | ||||||
|           --mdc-icon-size: 14px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .breadcrumb .subtitle { |  | ||||||
|           font-size: 16px; |  | ||||||
|           overflow: hidden; |  | ||||||
|           text-overflow: ellipsis; |  | ||||||
|           margin-bottom: 0; |  | ||||||
|           transition: height 0.5s, margin 0.5s; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /* ============= CHILDREN ============= */ |  | ||||||
|  |  | ||||||
|         mwc-list { |  | ||||||
|           --mdc-list-vertical-padding: 0; |  | ||||||
|           --mdc-list-item-graphic-margin: 0; |  | ||||||
|           --mdc-theme-text-icon-on-background: var(--secondary-text-color); |  | ||||||
|           margin-top: 10px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         mwc-list li:last-child { |  | ||||||
|           display: none; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         mwc-list li[divider] { |  | ||||||
|           border-bottom-color: var(--divider-color); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .children { |  | ||||||
|           display: grid; |  | ||||||
|           grid-template-columns: repeat( |  | ||||||
|             auto-fit, |  | ||||||
|             minmax(var(--media-browse-item-size, 175px), 0.33fr) |  | ||||||
|           ); |  | ||||||
|           grid-gap: 16px; |  | ||||||
|           margin: 8px 0px; |  | ||||||
|           padding: 0px 24px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .child { |  | ||||||
|           display: flex; |  | ||||||
|           flex-direction: column; |  | ||||||
|           cursor: pointer; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .ha-card-parent { |  | ||||||
|           position: relative; |  | ||||||
|           width: 100%; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         ha-card { |  | ||||||
|           width: 100%; |  | ||||||
|           padding-bottom: 100%; |  | ||||||
|           position: relative; |  | ||||||
|           box-sizing: border-box; |  | ||||||
|           background-size: cover; |  | ||||||
|           background-repeat: no-repeat; |  | ||||||
|           background-position: center; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .child .folder, |  | ||||||
|         .child .play { |  | ||||||
|           position: absolute; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .child .folder { |  | ||||||
|           color: var(--secondary-text-color); |  | ||||||
|           top: calc(50% - (var(--mdc-icon-size) / 2)); |  | ||||||
|           left: calc(50% - (var(--mdc-icon-size) / 2)); |  | ||||||
|           --mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .child .play { |  | ||||||
|           bottom: 4px; |  | ||||||
|           right: 4px; |  | ||||||
|           transition: all 0.5s; |  | ||||||
|           background-color: rgba(var(--rgb-card-background-color), 0.5); |  | ||||||
|           border-radius: 50%; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .child .play:hover { |  | ||||||
|           color: var(--primary-color); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         ha-card:hover { |  | ||||||
|           opacity: 0.5; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .child .title { |  | ||||||
|           font-size: 16px; |  | ||||||
|           padding-top: 8px; |  | ||||||
|           overflow: hidden; |  | ||||||
|           display: -webkit-box; |  | ||||||
|           -webkit-box-orient: vertical; |  | ||||||
|           -webkit-line-clamp: 2; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .child .type { |  | ||||||
|           font-size: 12px; |  | ||||||
|           color: var(--secondary-text-color); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         mwc-list-item .graphic { |  | ||||||
|           background-size: cover; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         mwc-list-item .graphic .play { |  | ||||||
|           opacity: 0; |  | ||||||
|           transition: all 0.5s; |  | ||||||
|           background-color: rgba(var(--rgb-card-background-color), 0.5); |  | ||||||
|           border-radius: 50%; |  | ||||||
|           --mdc-icon-button-size: 40px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         mwc-list-item:hover .graphic .play { |  | ||||||
|           opacity: 1; |  | ||||||
|           color: var(--primary-color); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         mwc-list-item .graphic .play.show { |  | ||||||
|           opacity: 1; |  | ||||||
|           background-color: transparent; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         mwc-list-item .title { |  | ||||||
|           margin-left: 16px; |  | ||||||
|         } |  | ||||||
|         mwc-list-item[dir="rtl"] .title { |  | ||||||
|           margin-right: 16px; |  | ||||||
|           margin-left: 0; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /* ============= Narrow ============= */ |  | ||||||
|  |  | ||||||
|         :host([narrow]) { |  | ||||||
|           padding: 0; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([narrow]) .breadcrumb .title { |  | ||||||
|           font-size: 24px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([narrow]) .header { |  | ||||||
|           padding: 0; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([narrow]) .header.no-dialog { |  | ||||||
|           display: block; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([narrow]) .header_button { |  | ||||||
|           position: absolute; |  | ||||||
|           top: 14px; |  | ||||||
|           right: 8px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([narrow]) .header-content { |  | ||||||
|           flex-direction: column; |  | ||||||
|           flex-wrap: nowrap; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([narrow]) .header-content .img { |  | ||||||
|           height: auto; |  | ||||||
|           width: 100%; |  | ||||||
|           margin-right: 0; |  | ||||||
|           padding-bottom: 50%; |  | ||||||
|           margin-bottom: 8px; |  | ||||||
|           position: relative; |  | ||||||
|           background-position: center; |  | ||||||
|           border-radius: 0; |  | ||||||
|           transition: width 0.4s, height 0.4s, padding-bottom 0.4s; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         mwc-fab { |  | ||||||
|           position: absolute; |  | ||||||
|           --mdc-theme-secondary: var(--primary-color); |  | ||||||
|           bottom: -20px; |  | ||||||
|           right: 20px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([narrow]) .header-info mwc-button { |  | ||||||
|           margin-top: 16px; |  | ||||||
|           margin-bottom: 8px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([narrow]) .header-info { |  | ||||||
|           padding: 20px 24px 10px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([narrow]) .media-source { |  | ||||||
|           padding: 0 24px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([narrow]) .children { |  | ||||||
|           grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /* ============= Scroll ============= */ |  | ||||||
|  |  | ||||||
|         :host([scroll]) .breadcrumb .subtitle { |  | ||||||
|           height: 0; |  | ||||||
|           margin: 0; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([scroll]) .breadcrumb .title { |  | ||||||
|           -webkit-line-clamp: 1; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host(:not([narrow])[scroll]) .header:not(.no-img) mwc-icon-button { |  | ||||||
|           align-self: center; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([scroll]) .header-info mwc-button, |  | ||||||
|         .no-img .header-info mwc-button { |  | ||||||
|           padding-right: 4px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([scroll][narrow]) .no-img .header-info mwc-button { |  | ||||||
|           padding-right: 16px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([scroll]) .header-info { |  | ||||||
|           flex-direction: row; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([scroll]) .header-info mwc-button { |  | ||||||
|           align-self: center; |  | ||||||
|           margin-top: 0; |  | ||||||
|           margin-bottom: 0; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([scroll][narrow]) .no-img .header-info { |  | ||||||
|           flex-direction: row-reverse; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([scroll][narrow]) .header-info { |  | ||||||
|           padding: 20px 24px 10px 24px; |  | ||||||
|           align-items: center; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([scroll]) .header-content { |  | ||||||
|           align-items: flex-end; |  | ||||||
|           flex-direction: row; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([scroll]) .header-content .img { |  | ||||||
|           height: 75px; |  | ||||||
|           width: 75px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([scroll][narrow]) .header-content .img { |  | ||||||
|           height: 100px; |  | ||||||
|           width: 100px; |  | ||||||
|           padding-bottom: initial; |  | ||||||
|           margin-bottom: 0; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         :host([scroll]) mwc-fab { |  | ||||||
|           bottom: 4px; |  | ||||||
|           right: 4px; |  | ||||||
|           --mdc-fab-box-shadow: none; |  | ||||||
|           --mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5); |  | ||||||
|         } |  | ||||||
|       `, |  | ||||||
|     ]; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "ha-media-player-browse": HaMediaPlayerBrowse; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,27 +0,0 @@ | |||||||
| import { fireEvent } from "../../common/dom/fire_event"; |  | ||||||
| import { |  | ||||||
|   MediaPickedEvent, |  | ||||||
|   MediaPlayerBrowseAction, |  | ||||||
| } from "../../data/media-player"; |  | ||||||
|  |  | ||||||
| export interface MediaPlayerBrowseDialogParams { |  | ||||||
|   action: MediaPlayerBrowseAction; |  | ||||||
|   entityId: string; |  | ||||||
|   mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void; |  | ||||||
|   mediaContentId?: string; |  | ||||||
|   mediaContentType?: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const showMediaBrowserDialog = ( |  | ||||||
|   element: HTMLElement, |  | ||||||
|   dialogParams: MediaPlayerBrowseDialogParams |  | ||||||
| ): void => { |  | ||||||
|   fireEvent(element, "show-dialog", { |  | ||||||
|     dialogTag: "dialog-media-player-browse", |  | ||||||
|     dialogImport: () => |  | ||||||
|       import( |  | ||||||
|         /* webpackChunkName: "dialog-media-player-browse" */ "./dialog-media-player-browse" |  | ||||||
|       ), |  | ||||||
|     dialogParams, |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| @@ -19,7 +19,6 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) { | |||||||
|       </style> |       </style> | ||||||
|       <ha-chart-base |       <ha-chart-base | ||||||
|         id="chart" |         id="chart" | ||||||
|         hass="[[hass]]" |  | ||||||
|         data="[[chartData]]" |         data="[[chartData]]" | ||||||
|         identifier="[[identifier]]" |         identifier="[[identifier]]" | ||||||
|         rendered="{{rendered}}" |         rendered="{{rendered}}" | ||||||
| @@ -29,9 +28,6 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) { | |||||||
|  |  | ||||||
|   static get properties() { |   static get properties() { | ||||||
|     return { |     return { | ||||||
|       hass: { |  | ||||||
|         type: Object, |  | ||||||
|       }, |  | ||||||
|       chartData: Object, |       chartData: Object, | ||||||
|       data: Object, |       data: Object, | ||||||
|       names: Object, |       names: Object, | ||||||
|   | |||||||
| @@ -25,7 +25,6 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) { | |||||||
|         } |         } | ||||||
|       </style> |       </style> | ||||||
|       <ha-chart-base |       <ha-chart-base | ||||||
|         hass="[[hass]]" |  | ||||||
|         data="[[chartData]]" |         data="[[chartData]]" | ||||||
|         rendered="{{rendered}}" |         rendered="{{rendered}}" | ||||||
|         rtl="{{rtl}}" |         rtl="{{rtl}}" | ||||||
| @@ -76,8 +75,6 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) { | |||||||
|     const staticColors = { |     const staticColors = { | ||||||
|       on: 1, |       on: 1, | ||||||
|       off: 0, |       off: 0, | ||||||
|       home: 1, |  | ||||||
|       not_home: 0, |  | ||||||
|       unavailable: "#a0a0a0", |       unavailable: "#a0a0a0", | ||||||
|       unknown: "#606060", |       unknown: "#606060", | ||||||
|       idle: 2, |       idle: 2, | ||||||
|   | |||||||
| @@ -1,74 +0,0 @@ | |||||||
| import { |  | ||||||
|   css, |  | ||||||
|   CSSResult, |  | ||||||
|   customElement, |  | ||||||
|   html, |  | ||||||
|   LitElement, |  | ||||||
|   property, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit-element"; |  | ||||||
| import { classMap } from "lit-html/directives/class-map"; |  | ||||||
| import { styleMap } from "lit-html/directives/style-map"; |  | ||||||
| import { Person } from "../../data/person"; |  | ||||||
| import { computeInitials } from "./ha-user-badge"; |  | ||||||
|  |  | ||||||
| @customElement("ha-person-badge") |  | ||||||
| class PersonBadge extends LitElement { |  | ||||||
|   @property({ attribute: false }) public person?: Person; |  | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |  | ||||||
|     if (!this.person) { |  | ||||||
|       return html``; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const picture = this.person.picture; |  | ||||||
|  |  | ||||||
|     if (picture) { |  | ||||||
|       return html`<div |  | ||||||
|         style=${styleMap({ backgroundImage: `url(${picture})` })} |  | ||||||
|         class="picture" |  | ||||||
|       ></div>`; |  | ||||||
|     } |  | ||||||
|     const initials = computeInitials(this.person.name); |  | ||||||
|     return html`<div |  | ||||||
|       class="initials ${classMap({ long: initials!.length > 2 })}" |  | ||||||
|     > |  | ||||||
|       ${initials} |  | ||||||
|     </div>`; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult { |  | ||||||
|     return css` |  | ||||||
|       :host { |  | ||||||
|         display: contents; |  | ||||||
|       } |  | ||||||
|       .picture { |  | ||||||
|         width: 40px; |  | ||||||
|         height: 40px; |  | ||||||
|         background-size: cover; |  | ||||||
|         border-radius: 50%; |  | ||||||
|       } |  | ||||||
|       .initials { |  | ||||||
|         display: inline-block; |  | ||||||
|         box-sizing: border-box; |  | ||||||
|         width: 40px; |  | ||||||
|         line-height: 40px; |  | ||||||
|         border-radius: 50%; |  | ||||||
|         text-align: center; |  | ||||||
|         background-color: var(--light-primary-color); |  | ||||||
|         text-decoration: none; |  | ||||||
|         color: var(--text-light-primary-color, var(--primary-text-color)); |  | ||||||
|         overflow: hidden; |  | ||||||
|       } |  | ||||||
|       .initials.long { |  | ||||||
|         font-size: 80%; |  | ||||||
|       } |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "ha-person-badge": PersonBadge; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -3,20 +3,17 @@ import { | |||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { classMap } from "lit-html/directives/class-map"; | import { toggleAttribute } from "../../common/dom/toggle_attribute"; | ||||||
| import { styleMap } from "lit-html/directives/style-map"; |  | ||||||
| import { computeStateDomain } from "../../common/entity/compute_state_domain"; |  | ||||||
| import { User } from "../../data/user"; | import { User } from "../../data/user"; | ||||||
| import { CurrentUser, HomeAssistant } from "../../types"; | import { CurrentUser } from "../../types"; | ||||||
|  |  | ||||||
| export const computeInitials = (name: string) => { | const computeInitials = (name: string) => { | ||||||
|   if (!name) { |   if (!name) { | ||||||
|     return "?"; |     return "user"; | ||||||
|   } |   } | ||||||
|   return ( |   return ( | ||||||
|     name |     name | ||||||
| @@ -31,89 +28,27 @@ export const computeInitials = (name: string) => { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| @customElement("ha-user-badge") | @customElement("ha-user-badge") | ||||||
| class UserBadge extends LitElement { | class StateBadge extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property() public user?: User | CurrentUser; | ||||||
|  |  | ||||||
|   @property({ attribute: false }) public user?: User | CurrentUser; |   protected render(): TemplateResult { | ||||||
|  |     const user = this.user; | ||||||
|   @internalProperty() private _personPicture?: string; |     const initials = user ? computeInitials(user.name) : "?"; | ||||||
|  |     return html` ${initials} `; | ||||||
|   private _personEntityId?: string; |   } | ||||||
|  |  | ||||||
|   protected updated(changedProps) { |   protected updated(changedProps) { | ||||||
|     super.updated(changedProps); |     super.updated(changedProps); | ||||||
|     if (changedProps.has("user")) { |     toggleAttribute( | ||||||
|       this._getPersonPicture(); |       this, | ||||||
|       return; |       "long", | ||||||
|     } |       (this.user ? computeInitials(this.user.name) : "?").length > 2 | ||||||
|     const oldHass = changedProps.get("hass"); |     ); | ||||||
|     if ( |  | ||||||
|       this._personEntityId && |  | ||||||
|       oldHass && |  | ||||||
|       this.hass.states[this._personEntityId] !== |  | ||||||
|         oldHass.states[this._personEntityId] |  | ||||||
|     ) { |  | ||||||
|       const state = this.hass.states[this._personEntityId]; |  | ||||||
|       if (state) { |  | ||||||
|         this._personPicture = state.attributes.entity_picture; |  | ||||||
|       } else { |  | ||||||
|         this._getPersonPicture(); |  | ||||||
|       } |  | ||||||
|     } else if (!this._personEntityId && oldHass) { |  | ||||||
|       this._getPersonPicture(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |  | ||||||
|     if (!this.hass || !this.user) { |  | ||||||
|       return html``; |  | ||||||
|     } |  | ||||||
|     const picture = this._personPicture; |  | ||||||
|  |  | ||||||
|     if (picture) { |  | ||||||
|       return html`<div |  | ||||||
|         style=${styleMap({ backgroundImage: `url(${picture})` })} |  | ||||||
|         class="picture" |  | ||||||
|       ></div>`; |  | ||||||
|     } |  | ||||||
|     const initials = computeInitials(this.user.name); |  | ||||||
|     return html`<div |  | ||||||
|       class="initials ${classMap({ long: initials!.length > 2 })}" |  | ||||||
|     > |  | ||||||
|       ${initials} |  | ||||||
|     </div>`; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _getPersonPicture() { |  | ||||||
|     this._personEntityId = undefined; |  | ||||||
|     this._personPicture = undefined; |  | ||||||
|     if (!this.hass || !this.user) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     for (const entity of Object.values(this.hass.states)) { |  | ||||||
|       if ( |  | ||||||
|         entity.attributes.user_id === this.user.id && |  | ||||||
|         computeStateDomain(entity) === "person" |  | ||||||
|       ) { |  | ||||||
|         this._personEntityId = entity.entity_id; |  | ||||||
|         this._personPicture = entity.attributes.entity_picture; |  | ||||||
|         break; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResult { |   static get styles(): CSSResult { | ||||||
|     return css` |     return css` | ||||||
|       :host { |       :host { | ||||||
|         display: contents; |  | ||||||
|       } |  | ||||||
|       .picture { |  | ||||||
|         width: 40px; |  | ||||||
|         height: 40px; |  | ||||||
|         background-size: cover; |  | ||||||
|         border-radius: 50%; |  | ||||||
|       } |  | ||||||
|       .initials { |  | ||||||
|         display: inline-block; |         display: inline-block; | ||||||
|         box-sizing: border-box; |         box-sizing: border-box; | ||||||
|         width: 40px; |         width: 40px; | ||||||
| @@ -125,7 +60,8 @@ class UserBadge extends LitElement { | |||||||
|         color: var(--text-light-primary-color, var(--primary-text-color)); |         color: var(--text-light-primary-color, var(--primary-text-color)); | ||||||
|         overflow: hidden; |         overflow: hidden; | ||||||
|       } |       } | ||||||
|       .initials.long { |  | ||||||
|  |       :host([long]) { | ||||||
|         font-size: 80%; |         font-size: 80%; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
| @@ -134,6 +70,6 @@ class UserBadge extends LitElement { | |||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
|     "ha-user-badge": UserBadge; |     "ha-user-badge": StateBadge; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -53,11 +53,7 @@ class HaUserPicker extends LitElement { | |||||||
|           ${this._sortedUsers(this.users).map( |           ${this._sortedUsers(this.users).map( | ||||||
|             (user) => html` |             (user) => html` | ||||||
|               <paper-icon-item data-user-id=${user.id}> |               <paper-icon-item data-user-id=${user.id}> | ||||||
|                 <ha-user-badge |                 <ha-user-badge .user=${user} slot="item-icon"></ha-user-badge> | ||||||
|                   .hass=${this.hass} |  | ||||||
|                   .user=${user} |  | ||||||
|                   slot="item-icon" |  | ||||||
|                 ></ha-user-badge> |  | ||||||
|                 ${user.name} |                 ${user.name} | ||||||
|               </paper-icon-item> |               </paper-icon-item> | ||||||
|             ` |             ` | ||||||
|   | |||||||
| @@ -44,14 +44,3 @@ export const createAuthForUser = async ( | |||||||
|     username, |     username, | ||||||
|     password, |     password, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
| export const adminChangePassword = async ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   userId: string, |  | ||||||
|   password: string |  | ||||||
| ) => |  | ||||||
|   hass.callWS<void>({ |  | ||||||
|     type: "config/auth_provider/homeassistant/admin_change_password", |  | ||||||
|     user_id: userId, |  | ||||||
|     password, |  | ||||||
|   }); |  | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import { | |||||||
|   HassEntityBase, |   HassEntityBase, | ||||||
| } from "home-assistant-js-websocket"; | } from "home-assistant-js-websocket"; | ||||||
| import { navigate } from "../common/navigate"; | import { navigate } from "../common/navigate"; | ||||||
| import { Context, HomeAssistant } from "../types"; | import { HomeAssistant } from "../types"; | ||||||
| import { DeviceCondition, DeviceTrigger } from "./device_automation"; | import { DeviceCondition, DeviceTrigger } from "./device_automation"; | ||||||
| import { Action } from "./script"; | import { Action } from "./script"; | ||||||
|  |  | ||||||
| @@ -15,7 +15,6 @@ export interface AutomationEntity extends HassEntityBase { | |||||||
| } | } | ||||||
|  |  | ||||||
| export interface AutomationConfig { | export interface AutomationConfig { | ||||||
|   id?: string; |  | ||||||
|   alias: string; |   alias: string; | ||||||
|   description: string; |   description: string; | ||||||
|   trigger: Trigger[]; |   trigger: Trigger[]; | ||||||
| @@ -33,8 +32,7 @@ export interface ForDict { | |||||||
|  |  | ||||||
| export interface StateTrigger { | export interface StateTrigger { | ||||||
|   platform: "state"; |   platform: "state"; | ||||||
|   entity_id: string; |   entity_id?: string; | ||||||
|   attribute?: string; |  | ||||||
|   from?: string | number; |   from?: string | number; | ||||||
|   to?: string | number; |   to?: string | number; | ||||||
|   for?: string | number | ForDict; |   for?: string | number | ForDict; | ||||||
| @@ -61,7 +59,6 @@ export interface HassTrigger { | |||||||
| export interface NumericStateTrigger { | export interface NumericStateTrigger { | ||||||
|   platform: "numeric_state"; |   platform: "numeric_state"; | ||||||
|   entity_id: string; |   entity_id: string; | ||||||
|   attribute?: string; |  | ||||||
|   above?: number; |   above?: number; | ||||||
|   below?: number; |   below?: number; | ||||||
|   value_template?: string; |   value_template?: string; | ||||||
| @@ -93,12 +90,6 @@ export interface ZoneTrigger { | |||||||
|   event: "enter" | "leave"; |   event: "enter" | "leave"; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface TagTrigger { |  | ||||||
|   platform: "tag"; |  | ||||||
|   tag_id: string; |  | ||||||
|   device_id?: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface TimeTrigger { | export interface TimeTrigger { | ||||||
|   platform: "time"; |   platform: "time"; | ||||||
|   at: string; |   at: string; | ||||||
| @@ -125,7 +116,6 @@ export type Trigger = | |||||||
|   | TimePatternTrigger |   | TimePatternTrigger | ||||||
|   | WebhookTrigger |   | WebhookTrigger | ||||||
|   | ZoneTrigger |   | ZoneTrigger | ||||||
|   | TagTrigger |  | ||||||
|   | TimeTrigger |   | TimeTrigger | ||||||
|   | TemplateTrigger |   | TemplateTrigger | ||||||
|   | EventTrigger |   | EventTrigger | ||||||
| @@ -139,14 +129,12 @@ export interface LogicalCondition { | |||||||
| export interface StateCondition { | export interface StateCondition { | ||||||
|   condition: "state"; |   condition: "state"; | ||||||
|   entity_id: string; |   entity_id: string; | ||||||
|   attribute?: string; |  | ||||||
|   state: string | number; |   state: string | number; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface NumericStateCondition { | export interface NumericStateCondition { | ||||||
|   condition: "numeric_state"; |   condition: "numeric_state"; | ||||||
|   entity_id: string; |   entity_id: string; | ||||||
|   attribute?: string; |  | ||||||
|   above?: number; |   above?: number; | ||||||
|   below?: number; |   below?: number; | ||||||
|   value_template?: string; |   value_template?: string; | ||||||
| @@ -211,31 +199,3 @@ export const getAutomationEditorInitData = () => { | |||||||
|   inititialAutomationEditorData = undefined; |   inititialAutomationEditorData = undefined; | ||||||
|   return data; |   return data; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const subscribeTrigger = ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   onChange: (result: { |  | ||||||
|     variables: { |  | ||||||
|       trigger: {}; |  | ||||||
|     }; |  | ||||||
|     context: Context; |  | ||||||
|   }) => void, |  | ||||||
|   trigger: Trigger | Trigger[], |  | ||||||
|   variables?: {} |  | ||||||
| ) => |  | ||||||
|   hass.connection.subscribeMessage(onChange, { |  | ||||||
|     type: "subscribe_trigger", |  | ||||||
|     trigger, |  | ||||||
|     variables, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
| export const testCondition = ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   condition: Condition | Condition[], |  | ||||||
|   variables?: {} |  | ||||||
| ) => |  | ||||||
|   hass.callWS<{ result: boolean }>({ |  | ||||||
|     type: "test_condition", |  | ||||||
|     condition, |  | ||||||
|     variables, |  | ||||||
|   }); |  | ||||||
|   | |||||||
| @@ -9,14 +9,14 @@ interface CloudStatusBase { | |||||||
| } | } | ||||||
|  |  | ||||||
| export interface GoogleEntityConfig { | export interface GoogleEntityConfig { | ||||||
|   should_expose?: boolean | null; |   should_expose?: boolean; | ||||||
|   override_name?: string; |   override_name?: string; | ||||||
|   aliases?: string[]; |   aliases?: string[]; | ||||||
|   disable_2fa?: boolean; |   disable_2fa?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface AlexaEntityConfig { | export interface AlexaEntityConfig { | ||||||
|   should_expose?: boolean | null; |   should_expose?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface CertificateInformation { | export interface CertificateInformation { | ||||||
| @@ -31,11 +31,9 @@ export interface CloudPreferences { | |||||||
|   remote_enabled: boolean; |   remote_enabled: boolean; | ||||||
|   google_secure_devices_pin: string | undefined; |   google_secure_devices_pin: string | undefined; | ||||||
|   cloudhooks: { [webhookId: string]: CloudWebhook }; |   cloudhooks: { [webhookId: string]: CloudWebhook }; | ||||||
|   google_default_expose: string[] | null; |  | ||||||
|   google_entity_configs: { |   google_entity_configs: { | ||||||
|     [entityId: string]: GoogleEntityConfig; |     [entityId: string]: GoogleEntityConfig; | ||||||
|   }; |   }; | ||||||
|   alexa_default_expose: string[] | null; |  | ||||||
|   alexa_entity_configs: { |   alexa_entity_configs: { | ||||||
|     [entityId: string]: AlexaEntityConfig; |     [entityId: string]: AlexaEntityConfig; | ||||||
|   }; |   }; | ||||||
| @@ -108,10 +106,8 @@ export const updateCloudPref = ( | |||||||
|   prefs: { |   prefs: { | ||||||
|     google_enabled?: CloudPreferences["google_enabled"]; |     google_enabled?: CloudPreferences["google_enabled"]; | ||||||
|     alexa_enabled?: CloudPreferences["alexa_enabled"]; |     alexa_enabled?: CloudPreferences["alexa_enabled"]; | ||||||
|     alexa_default_expose?: CloudPreferences["alexa_default_expose"]; |  | ||||||
|     alexa_report_state?: CloudPreferences["alexa_report_state"]; |     alexa_report_state?: CloudPreferences["alexa_report_state"]; | ||||||
|     google_report_state?: CloudPreferences["google_report_state"]; |     google_report_state?: CloudPreferences["google_report_state"]; | ||||||
|     google_default_expose?: CloudPreferences["google_default_expose"]; |  | ||||||
|     google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"]; |     google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"]; | ||||||
|   } |   } | ||||||
| ) => | ) => | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ export interface ConfigEntry { | |||||||
|   state: string; |   state: string; | ||||||
|   connection_class: string; |   connection_class: string; | ||||||
|   supports_options: boolean; |   supports_options: boolean; | ||||||
|   supports_unload: boolean; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface ConfigEntryMutableParams { | export interface ConfigEntryMutableParams { | ||||||
| @@ -38,11 +37,6 @@ export const deleteConfigEntry = (hass: HomeAssistant, configEntryId: string) => | |||||||
|     require_restart: boolean; |     require_restart: boolean; | ||||||
|   }>("DELETE", `config/config_entries/entry/${configEntryId}`); |   }>("DELETE", `config/config_entries/entry/${configEntryId}`); | ||||||
|  |  | ||||||
| export const reloadConfigEntry = (hass: HomeAssistant, configEntryId: string) => |  | ||||||
|   hass.callApi<{ |  | ||||||
|     require_restart: boolean; |  | ||||||
|   }>("POST", `config/config_entries/entry/${configEntryId}/reload`); |  | ||||||
|  |  | ||||||
| export const getConfigEntrySystemOptions = ( | export const getConfigEntrySystemOptions = ( | ||||||
|   hass: HomeAssistant, |   hass: HomeAssistant, | ||||||
|   configEntryId: string |   configEntryId: string | ||||||
|   | |||||||
| @@ -13,8 +13,6 @@ export const DISCOVERY_SOURCES = [ | |||||||
|   "discovery", |   "discovery", | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| export const ATTENTION_SOURCES = ["reauth"]; |  | ||||||
|  |  | ||||||
| export const createConfigFlow = (hass: HomeAssistant, handler: string) => | export const createConfigFlow = (hass: HomeAssistant, handler: string) => | ||||||
|   hass.callApi<DataEntryFlowStep>("POST", "config/config_entries/flow", { |   hass.callApi<DataEntryFlowStep>("POST", "config/config_entries/flow", { | ||||||
|     handler, |     handler, | ||||||
|   | |||||||
| @@ -51,7 +51,6 @@ export interface HassioAddonDetails extends HassioAddonInfo { | |||||||
|   changelog: boolean; |   changelog: boolean; | ||||||
|   hassio_api: boolean; |   hassio_api: boolean; | ||||||
|   hassio_role: "default" | "homeassistant" | "manager" | "admin"; |   hassio_role: "default" | "homeassistant" | "manager" | "admin"; | ||||||
|   startup: "initialize" | "system" | "services" | "application" | "once"; |  | ||||||
|   homeassistant_api: boolean; |   homeassistant_api: boolean; | ||||||
|   auth_api: boolean; |   auth_api: boolean; | ||||||
|   full_access: boolean; |   full_access: boolean; | ||||||
| @@ -73,7 +72,6 @@ export interface HassioAddonDetails extends HassioAddonInfo { | |||||||
|   ingress_panel: boolean; |   ingress_panel: boolean; | ||||||
|   ingress_entry: null | string; |   ingress_entry: null | string; | ||||||
|   ingress_url: null | string; |   ingress_url: null | string; | ||||||
|   watchdog: null | boolean; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface HassioAddonsInfo { | export interface HassioAddonsInfo { | ||||||
| @@ -101,7 +99,6 @@ export interface HassioAddonSetOptionParams { | |||||||
|   auto_update?: boolean; |   auto_update?: boolean; | ||||||
|   ingress_panel?: boolean; |   ingress_panel?: boolean; | ||||||
|   network?: object | null; |   network?: object | null; | ||||||
|   watchdog?: boolean; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export const reloadHassioAddons = async (hass: HomeAssistant) => { | export const reloadHassioAddons = async (hass: HomeAssistant) => { | ||||||
| @@ -159,19 +156,6 @@ export const setHassioAddonOption = async ( | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const validateHassioAddonOption = async ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   slug: string |  | ||||||
| ) => { |  | ||||||
|   return await hass.callApi< |  | ||||||
|     HassioResponse<{ message: string; valid: boolean }> |  | ||||||
|   >("POST", `hassio/addons/${slug}/options/validate`); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const startHassioAddon = async (hass: HomeAssistant, slug: string) => { |  | ||||||
|   return hass.callApi<string>("POST", `hassio/addons/${slug}/start`); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const setHassioAddonSecurity = async ( | export const setHassioAddonSecurity = async ( | ||||||
|   hass: HomeAssistant, |   hass: HomeAssistant, | ||||||
|   slug: string, |   slug: string, | ||||||
|   | |||||||
| @@ -5,11 +5,3 @@ export interface HassioResponse<T> { | |||||||
|  |  | ||||||
| export const hassioApiResultExtractor = <T>(response: HassioResponse<T>) => | export const hassioApiResultExtractor = <T>(response: HassioResponse<T>) => | ||||||
|   response.data; |   response.data; | ||||||
|  |  | ||||||
| export const extractApiErrorMessage = (error: any): string => { |  | ||||||
|   return typeof error === "object" |  | ||||||
|     ? typeof error.body === "object" |  | ||||||
|       ? error.body.message || "Unknown error, see logs" |  | ||||||
|       : error.body || "Unknown error, see logs" |  | ||||||
|     : error; |  | ||||||
| }; |  | ||||||
|   | |||||||
| @@ -40,10 +40,6 @@ export const updateOS = async (hass: HomeAssistant) => { | |||||||
|   return hass.callApi<HassioResponse<void>>("POST", "hassio/os/update"); |   return hass.callApi<HassioResponse<void>>("POST", "hassio/os/update"); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const configSyncOS = async (hass: HomeAssistant) => { |  | ||||||
|   return hass.callApi<HassioResponse<void>>("POST", "hassio/os/config/sync"); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const changeHostOptions = async (hass: HomeAssistant, options: any) => { | export const changeHostOptions = async (hass: HomeAssistant, options: any) => { | ||||||
|   return hass.callApi<HassioResponse<void>>( |   return hass.callApi<HassioResponse<void>>( | ||||||
|     "POST", |     "POST", | ||||||
|   | |||||||
| @@ -1,43 +0,0 @@ | |||||||
| import { HomeAssistant } from "../../types"; |  | ||||||
| import { hassioApiResultExtractor, HassioResponse } from "./common"; |  | ||||||
|  |  | ||||||
| export interface NetworkInterface { |  | ||||||
|   gateway: string; |  | ||||||
|   id: string; |  | ||||||
|   ip_address: string; |  | ||||||
|   address?: string; |  | ||||||
|   method: "static" | "dhcp"; |  | ||||||
|   nameservers: string[] | string; |  | ||||||
|   dns?: string[]; |  | ||||||
|   primary: boolean; |  | ||||||
|   type: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface NetworkInterfaces { |  | ||||||
|   [key: string]: NetworkInterface; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface NetworkInfo { |  | ||||||
|   interfaces: NetworkInterfaces; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const fetchNetworkInfo = async (hass: HomeAssistant) => { |  | ||||||
|   return hassioApiResultExtractor( |  | ||||||
|     await hass.callApi<HassioResponse<NetworkInfo>>( |  | ||||||
|       "GET", |  | ||||||
|       "hassio/network/info" |  | ||||||
|     ) |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const updateNetworkInterface = async ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   network_interface: string, |  | ||||||
|   options: Partial<NetworkInterface> |  | ||||||
| ) => { |  | ||||||
|   await hass.callApi<HassioResponse<NetworkInfo>>( |  | ||||||
|     "POST", |  | ||||||
|     `hassio/network/interface/${network_interface}/update`, |  | ||||||
|     options |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| @@ -35,14 +35,6 @@ export interface SupervisorOptions { | |||||||
|   addons_repositories?: string[]; |   addons_repositories?: string[]; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const reloadSupervisor = async (hass: HomeAssistant) => { |  | ||||||
|   await hass.callApi<HassioResponse<void>>("POST", `hassio/supervisor/reload`); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const updateSupervisor = async (hass: HomeAssistant) => { |  | ||||||
|   await hass.callApi<HassioResponse<void>>("POST", `hassio/supervisor/update`); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => { | export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => { | ||||||
|   return hassioApiResultExtractor( |   return hassioApiResultExtractor( | ||||||
|     await hass.callApi<HassioResponse<HassioHomeAssistantInfo>>( |     await hass.callApi<HassioResponse<HassioHomeAssistantInfo>>( | ||||||
| @@ -79,11 +71,7 @@ export const createHassioSession = async (hass: HomeAssistant) => { | |||||||
|     "POST", |     "POST", | ||||||
|     "hassio/ingress/session" |     "hassio/ingress/session" | ||||||
|   ); |   ); | ||||||
|   document.cookie = `ingress_session=${ |   document.cookie = `ingress_session=${response.data.session};path=/api/hassio_ingress/`; | ||||||
|     response.data.session |  | ||||||
|   };path=/api/hassio_ingress/;SameSite=Strict${ |  | ||||||
|     location.protocol === "https:" ? ";Secure" : "" |  | ||||||
|   }`; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const setSupervisorOption = async ( | export const setSupervisorOption = async ( | ||||||
|   | |||||||
| @@ -1,54 +0,0 @@ | |||||||
| import { HomeAssistant } from "../types"; |  | ||||||
|  |  | ||||||
| interface Image { |  | ||||||
|   filesize: number; |  | ||||||
|   name: string; |  | ||||||
|   uploaded_at: string; // isoformat date |  | ||||||
|   content_type: string; |  | ||||||
|   id: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface ImageMutableParams { |  | ||||||
|   name: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const generateImageThumbnailUrl = (mediaId: string, size: number) => |  | ||||||
|   `/api/image/serve/${mediaId}/${size}x${size}`; |  | ||||||
|  |  | ||||||
| export const fetchImages = (hass: HomeAssistant) => |  | ||||||
|   hass.callWS<Image[]>({ type: "image/list" }); |  | ||||||
|  |  | ||||||
| export const createImage = async ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   file: File |  | ||||||
| ): Promise<Image> => { |  | ||||||
|   const fd = new FormData(); |  | ||||||
|   fd.append("file", file); |  | ||||||
|   const resp = await hass.fetchWithAuth("/api/image/upload", { |  | ||||||
|     method: "POST", |  | ||||||
|     body: fd, |  | ||||||
|   }); |  | ||||||
|   if (resp.status === 413) { |  | ||||||
|     throw new Error("Uploaded image is too large"); |  | ||||||
|   } else if (resp.status !== 200) { |  | ||||||
|     throw new Error("Unknown error"); |  | ||||||
|   } |  | ||||||
|   return await resp.json(); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const updateImage = ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   id: string, |  | ||||||
|   updates: Partial<ImageMutableParams> |  | ||||||
| ) => |  | ||||||
|   hass.callWS<Image>({ |  | ||||||
|     type: "image/update", |  | ||||||
|     media_id: id, |  | ||||||
|     ...updates, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
| export const deleteImage = (hass: HomeAssistant, id: string) => |  | ||||||
|   hass.callWS({ |  | ||||||
|     type: "image/delete", |  | ||||||
|     media_id: id, |  | ||||||
|   }); |  | ||||||
| @@ -7,12 +7,6 @@ export interface LogbookEntry { | |||||||
|   entity_id?: string; |   entity_id?: string; | ||||||
|   domain: string; |   domain: string; | ||||||
|   context_user_id?: string; |   context_user_id?: string; | ||||||
|   context_event_type?: string; |  | ||||||
|   context_domain?: string; |  | ||||||
|   context_service?: string; |  | ||||||
|   context_entity_id?: string; |  | ||||||
|   context_entity_id_name?: string; |  | ||||||
|   context_name?: string; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| const DATA_CACHE: { | const DATA_CACHE: { | ||||||
| @@ -23,8 +17,7 @@ export const getLogbookData = ( | |||||||
|   hass: HomeAssistant, |   hass: HomeAssistant, | ||||||
|   startDate: string, |   startDate: string, | ||||||
|   endDate: string, |   endDate: string, | ||||||
|   entityId?: string, |   entityId?: string | ||||||
|   entity_matches_only?: boolean |  | ||||||
| ) => { | ) => { | ||||||
|   const ALL_ENTITIES = "*"; |   const ALL_ENTITIES = "*"; | ||||||
|  |  | ||||||
| @@ -52,8 +45,7 @@ export const getLogbookData = ( | |||||||
|     hass, |     hass, | ||||||
|     startDate, |     startDate, | ||||||
|     endDate, |     endDate, | ||||||
|     entityId !== ALL_ENTITIES ? entityId : undefined, |     entityId !== ALL_ENTITIES ? entityId : undefined | ||||||
|     entity_matches_only |  | ||||||
|   ).then((entries) => entries.reverse()); |   ).then((entries) => entries.reverse()); | ||||||
|   return DATA_CACHE[cacheKey][entityId]; |   return DATA_CACHE[cacheKey][entityId]; | ||||||
| }; | }; | ||||||
| @@ -62,13 +54,11 @@ const getLogbookDataFromServer = async ( | |||||||
|   hass: HomeAssistant, |   hass: HomeAssistant, | ||||||
|   startDate: string, |   startDate: string, | ||||||
|   endDate: string, |   endDate: string, | ||||||
|   entityId?: string, |   entityId?: string | ||||||
|   entity_matches_only?: boolean |  | ||||||
| ) => { | ) => { | ||||||
|   const url = `logbook/${startDate}?end_time=${endDate}${ |   const url = `logbook/${startDate}?end_time=${endDate}${ | ||||||
|     entityId ? `&entity=${entityId}` : "" |     entityId ? `&entity=${entityId}` : "" | ||||||
|   }${entity_matches_only ? `&entity_matches_only` : ""}`; |   }`; | ||||||
|  |  | ||||||
|   return hass.callApi<LogbookEntry[]>("GET", url); |   return hass.callApi<LogbookEntry[]>("GET", url); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -318,11 +318,10 @@ export interface WindowWithLovelaceProm extends Window { | |||||||
| export interface ActionHandlerOptions { | export interface ActionHandlerOptions { | ||||||
|   hasHold?: boolean; |   hasHold?: boolean; | ||||||
|   hasDoubleClick?: boolean; |   hasDoubleClick?: boolean; | ||||||
|   disabled?: boolean; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface ActionHandlerDetail { | export interface ActionHandlerDetail { | ||||||
|   action: "hold" | "tap" | "double_tap"; |   action: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| export type ActionHandlerEvent = HASSDomEvent<ActionHandlerDetail>; | export type ActionHandlerEvent = HASSDomEvent<ActionHandlerDetail>; | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import type { HassEntity } from "home-assistant-js-websocket"; | import { HassEntity } from "home-assistant-js-websocket"; | ||||||
| import type { HomeAssistant } from "../types"; |  | ||||||
|  |  | ||||||
| export const SUPPORT_PAUSE = 1; | export const SUPPORT_PAUSE = 1; | ||||||
| export const SUPPORT_SEEK = 2; | export const SUPPORT_SEEK = 2; | ||||||
| @@ -15,59 +14,13 @@ export const SUPPORT_SELECT_SOURCE = 2048; | |||||||
| export const SUPPORT_STOP = 4096; | export const SUPPORT_STOP = 4096; | ||||||
| export const SUPPORTS_PLAY = 16384; | export const SUPPORTS_PLAY = 16384; | ||||||
| export const SUPPORT_SELECT_SOUND_MODE = 65536; | export const SUPPORT_SELECT_SOUND_MODE = 65536; | ||||||
| export const SUPPORT_BROWSE_MEDIA = 131072; |  | ||||||
| export const CONTRAST_RATIO = 4.5; | export const CONTRAST_RATIO = 4.5; | ||||||
|  |  | ||||||
| export type MediaPlayerBrowseAction = "pick" | "play"; |  | ||||||
|  |  | ||||||
| export const BROWSER_SOURCE = "browser"; |  | ||||||
|  |  | ||||||
| export interface MediaPickedEvent { |  | ||||||
|   item: MediaPlayerItem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface MediaPlayerThumbnail { | export interface MediaPlayerThumbnail { | ||||||
|   content_type: string; |   content_type: string; | ||||||
|   content: string; |   content: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface ControlButton { |  | ||||||
|   icon: string; |  | ||||||
|   action: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface MediaPlayerItem { |  | ||||||
|   title: string; |  | ||||||
|   media_content_type: string; |  | ||||||
|   media_content_id: string; |  | ||||||
|   can_play: boolean; |  | ||||||
|   can_expand: boolean; |  | ||||||
|   thumbnail?: string; |  | ||||||
|   children?: MediaPlayerItem[]; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const browseMediaPlayer = ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   entityId: string, |  | ||||||
|   mediaContentId?: string, |  | ||||||
|   mediaContentType?: string |  | ||||||
| ): Promise<MediaPlayerItem> => |  | ||||||
|   hass.callWS<MediaPlayerItem>({ |  | ||||||
|     type: "media_player/browse_media", |  | ||||||
|     entity_id: entityId, |  | ||||||
|     media_content_id: mediaContentId, |  | ||||||
|     media_content_type: mediaContentType, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
| export const browseLocalMediaPlayer = ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   mediaContentId?: string |  | ||||||
| ): Promise<MediaPlayerItem> => |  | ||||||
|   hass.callWS<MediaPlayerItem>({ |  | ||||||
|     type: "media_source/browse_media", |  | ||||||
|     media_content_id: mediaContentId, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
| export const getCurrentProgress = (stateObj: HassEntity): number => { | export const getCurrentProgress = (stateObj: HassEntity): number => { | ||||||
|   let progress = stateObj.attributes.media_position; |   let progress = stateObj.attributes.media_position; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										175
									
								
								src/data/ozw.ts
									
									
									
									
									
								
							
							
						
						
									
										175
									
								
								src/data/ozw.ts
									
									
									
									
									
								
							| @@ -1,10 +1,4 @@ | |||||||
| import { HomeAssistant } from "../types"; | import { HomeAssistant } from "../types"; | ||||||
| import { DeviceRegistryEntry } from "./device_registry"; |  | ||||||
|  |  | ||||||
| export interface OZWNodeIdentifiers { |  | ||||||
|   ozw_instance: number; |  | ||||||
|   node_id: number; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface OZWDevice { | export interface OZWDevice { | ||||||
|   node_id: number; |   node_id: number; | ||||||
| @@ -13,180 +7,15 @@ export interface OZWDevice { | |||||||
|   is_failed: boolean; |   is_failed: boolean; | ||||||
|   is_zwave_plus: boolean; |   is_zwave_plus: boolean; | ||||||
|   ozw_instance: number; |   ozw_instance: number; | ||||||
|   event: string; |  | ||||||
|   node_manufacturer_name: string; |  | ||||||
|   node_product_name: string; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface OZWDeviceMetaDataResponse { |  | ||||||
|   node_id: number; |  | ||||||
|   ozw_instance: number; |  | ||||||
|   metadata: OZWDeviceMetaData; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface OZWDeviceMetaData { |  | ||||||
|   OZWInfoURL: string; |  | ||||||
|   ZWAProductURL: string; |  | ||||||
|   ProductPic: string; |  | ||||||
|   Description: string; |  | ||||||
|   ProductManualURL: string; |  | ||||||
|   ProductPageURL: string; |  | ||||||
|   InclusionHelp: string; |  | ||||||
|   ExclusionHelp: string; |  | ||||||
|   ResetHelp: string; |  | ||||||
|   WakeupHelp: string; |  | ||||||
|   ProductSupportURL: string; |  | ||||||
|   Frequency: string; |  | ||||||
|   Name: string; |  | ||||||
|   ProductPicBase64: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface OZWInstance { |  | ||||||
|   ozw_instance: number; |  | ||||||
|   OZWDaemon_Version: string; |  | ||||||
|   OpenZWave_Version: string; |  | ||||||
|   QTOpenZWave_Version: string; |  | ||||||
|   Status: string; |  | ||||||
|   getControllerPath: string; |  | ||||||
|   homeID: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface OZWNetworkStatistics { |  | ||||||
|   ozw_instance: number; |  | ||||||
|   node_count: number; |  | ||||||
|   readCnt: number; |  | ||||||
|   writeCnt: number; |  | ||||||
|   ACKCnt: number; |  | ||||||
|   CANCnt: number; |  | ||||||
|   NAKCnt: number; |  | ||||||
|   dropped: number; |  | ||||||
|   retries: number; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const nodeQueryStages = [ |  | ||||||
|   "ProtocolInfo", |  | ||||||
|   "Probe", |  | ||||||
|   "WakeUp", |  | ||||||
|   "ManufacturerSpecific1", |  | ||||||
|   "NodeInfo", |  | ||||||
|   "NodePlusInfo", |  | ||||||
|   "ManufacturerSpecific2", |  | ||||||
|   "Versions", |  | ||||||
|   "Instances", |  | ||||||
|   "Static", |  | ||||||
|   "CacheLoad", |  | ||||||
|   "Associations", |  | ||||||
|   "Neighbors", |  | ||||||
|   "Session", |  | ||||||
|   "Dynamic", |  | ||||||
|   "Configuration", |  | ||||||
|   "Complete", |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| export const networkOnlineStatuses = [ |  | ||||||
|   "driverAllNodesQueried", |  | ||||||
|   "driverAllNodesQueriedSomeDead", |  | ||||||
|   "driverAwakeNodesQueried", |  | ||||||
| ]; |  | ||||||
| export const networkStartingStatuses = [ |  | ||||||
|   "starting", |  | ||||||
|   "started", |  | ||||||
|   "Ready", |  | ||||||
|   "driverReady", |  | ||||||
| ]; |  | ||||||
| export const networkOfflineStatuses = [ |  | ||||||
|   "Offline", |  | ||||||
|   "stopped", |  | ||||||
|   "driverFailed", |  | ||||||
|   "driverReset", |  | ||||||
|   "driverRemoved", |  | ||||||
|   "driverAllNodesOnFire", |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| export const getIdentifiersFromDevice = function ( |  | ||||||
|   device: DeviceRegistryEntry |  | ||||||
| ): OZWNodeIdentifiers | undefined { |  | ||||||
|   if (!device) { |  | ||||||
|     return undefined; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const ozwIdentifier = device.identifiers.find( |  | ||||||
|     (identifier) => identifier[0] === "ozw" |  | ||||||
|   ); |  | ||||||
|   if (!ozwIdentifier) { |  | ||||||
|     return undefined; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const identifiers = ozwIdentifier[1].split("."); |  | ||||||
|   return { |  | ||||||
|     node_id: parseInt(identifiers[1]), |  | ||||||
|     ozw_instance: parseInt(identifiers[0]), |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const fetchOZWInstances = ( |  | ||||||
|   hass: HomeAssistant |  | ||||||
| ): Promise<OZWInstance[]> => |  | ||||||
|   hass.callWS({ |  | ||||||
|     type: "ozw/get_instances", |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
| export const fetchOZWNetworkStatus = ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   ozw_instance: number |  | ||||||
| ): Promise<OZWInstance> => |  | ||||||
|   hass.callWS({ |  | ||||||
|     type: "ozw/network_status", |  | ||||||
|     ozw_instance: ozw_instance, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
| export const fetchOZWNetworkStatistics = ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   ozw_instance: number |  | ||||||
| ): Promise<OZWNetworkStatistics> => |  | ||||||
|   hass.callWS({ |  | ||||||
|     type: "ozw/network_statistics", |  | ||||||
|     ozw_instance: ozw_instance, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
| export const fetchOZWNodes = ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   ozw_instance: number |  | ||||||
| ): Promise<OZWDevice[]> => |  | ||||||
|   hass.callWS({ |  | ||||||
|     type: "ozw/get_nodes", |  | ||||||
|     ozw_instance: ozw_instance, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
| export const fetchOZWNodeStatus = ( | export const fetchOZWNodeStatus = ( | ||||||
|   hass: HomeAssistant, |   hass: HomeAssistant, | ||||||
|   ozw_instance: number, |   ozw_instance: string, | ||||||
|   node_id: number |   node_id: string | ||||||
| ): Promise<OZWDevice> => | ): Promise<OZWDevice> => | ||||||
|   hass.callWS({ |   hass.callWS({ | ||||||
|     type: "ozw/node_status", |     type: "ozw/node_status", | ||||||
|     ozw_instance: ozw_instance, |     ozw_instance: ozw_instance, | ||||||
|     node_id: node_id, |     node_id: node_id, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
| export const fetchOZWNodeMetadata = ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   ozw_instance: number, |  | ||||||
|   node_id: number |  | ||||||
| ): Promise<OZWDeviceMetaDataResponse> => |  | ||||||
|   hass.callWS({ |  | ||||||
|     type: "ozw/node_metadata", |  | ||||||
|     ozw_instance: ozw_instance, |  | ||||||
|     node_id: node_id, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
| export const refreshNodeInfo = ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   ozw_instance: number, |  | ||||||
|   node_id: number |  | ||||||
| ): Promise<OZWDevice> => |  | ||||||
|   hass.callWS({ |  | ||||||
|     type: "ozw/refresh_node_info", |  | ||||||
|     ozw_instance: ozw_instance, |  | ||||||
|     node_id: node_id, |  | ||||||
|   }); |  | ||||||
|   | |||||||
| @@ -17,9 +17,7 @@ export const setDefaultPanel = ( | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => | export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => | ||||||
|   hass.panels[hass.defaultPanel] |   hass.panels[hass.defaultPanel]; | ||||||
|     ? hass.panels[hass.defaultPanel] |  | ||||||
|     : hass.panels[DEFAULT_PANEL]; |  | ||||||
|  |  | ||||||
| export const getPanelTitle = (hass: HomeAssistant): string | undefined => { | export const getPanelTitle = (hass: HomeAssistant): string | undefined => { | ||||||
|   if (!hass.panels) { |   if (!hass.panels) { | ||||||
|   | |||||||
| @@ -5,14 +5,12 @@ export interface Person { | |||||||
|   name: string; |   name: string; | ||||||
|   user_id?: string; |   user_id?: string; | ||||||
|   device_trackers?: string[]; |   device_trackers?: string[]; | ||||||
|   picture?: string; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface PersonMutableParams { | export interface PersonMutableParams { | ||||||
|   name: string; |   name: string; | ||||||
|   user_id: string | null; |   user_id: string | null; | ||||||
|   device_trackers: string[]; |   device_trackers: string[]; | ||||||
|   picture: string | null; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export const fetchPersons = (hass: HomeAssistant) => | export const fetchPersons = (hass: HomeAssistant) => | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import { | |||||||
| import { computeObjectId } from "../common/entity/compute_object_id"; | import { computeObjectId } from "../common/entity/compute_object_id"; | ||||||
| import { navigate } from "../common/navigate"; | import { navigate } from "../common/navigate"; | ||||||
| import { HomeAssistant } from "../types"; | import { HomeAssistant } from "../types"; | ||||||
| import { Condition, Trigger } from "./automation"; | import { Condition } from "./automation"; | ||||||
|  |  | ||||||
| export const MODES = ["single", "restart", "queued", "parallel"]; | export const MODES = ["single", "restart", "queued", "parallel"]; | ||||||
| export const MODES_MAX = ["queued", "parallel"]; | export const MODES_MAX = ["queued", "parallel"]; | ||||||
| @@ -56,13 +56,6 @@ export interface SceneAction { | |||||||
| export interface WaitAction { | export interface WaitAction { | ||||||
|   wait_template: string; |   wait_template: string; | ||||||
|   timeout?: number; |   timeout?: number; | ||||||
|   continue_on_timeout?: boolean; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface WaitForTriggerAction { |  | ||||||
|   wait_for_trigger: Trigger[]; |  | ||||||
|   timeout?: number; |  | ||||||
|   continue_on_timeout?: boolean; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface RepeatAction { | export interface RepeatAction { | ||||||
| @@ -98,7 +91,6 @@ export type Action = | |||||||
|   | DelayAction |   | DelayAction | ||||||
|   | SceneAction |   | SceneAction | ||||||
|   | WaitAction |   | WaitAction | ||||||
|   | WaitForTriggerAction |  | ||||||
|   | RepeatAction |   | RepeatAction | ||||||
|   | ChooseAction; |   | ChooseAction; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,57 +0,0 @@ | |||||||
| import { HomeAssistant } from "../types"; |  | ||||||
| import { HassEventBase } from "home-assistant-js-websocket"; |  | ||||||
|  |  | ||||||
| export const EVENT_TAG_SCANNED = "tag_scanned"; |  | ||||||
|  |  | ||||||
| export interface TagScannedEvent extends HassEventBase { |  | ||||||
|   event_type: "tag_scanned"; |  | ||||||
|   data: { |  | ||||||
|     tag_id: string; |  | ||||||
|     device_id?: string; |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface Tag { |  | ||||||
|   id: string; |  | ||||||
|   name?: string; |  | ||||||
|   description?: string; |  | ||||||
|   last_scanned?: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface UpdateTagParams { |  | ||||||
|   name?: Tag["name"]; |  | ||||||
|   description?: Tag["description"]; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const fetchTags = async (hass: HomeAssistant) => |  | ||||||
|   hass.callWS<Tag[]>({ |  | ||||||
|     type: "tag/list", |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
| export const createTag = async ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   params: UpdateTagParams, |  | ||||||
|   tagId?: string |  | ||||||
| ) => |  | ||||||
|   hass.callWS<Tag>({ |  | ||||||
|     type: "tag/create", |  | ||||||
|     tag_id: tagId, |  | ||||||
|     ...params, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
| export const updateTag = async ( |  | ||||||
|   hass: HomeAssistant, |  | ||||||
|   tagId: string, |  | ||||||
|   params: UpdateTagParams |  | ||||||
| ) => |  | ||||||
|   hass.callWS<Tag>({ |  | ||||||
|     ...params, |  | ||||||
|     type: "tag/update", |  | ||||||
|     tag_id: tagId, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
| export const deleteTag = async (hass: HomeAssistant, tagId: string) => |  | ||||||
|   hass.callWS<void>({ |  | ||||||
|     type: "tag/delete", |  | ||||||
|     tag_id: tagId, |  | ||||||
|   }); |  | ||||||
| @@ -2,7 +2,6 @@ import { SVGTemplateResult, svg, html, TemplateResult, css } from "lit-element"; | |||||||
| import { styleMap } from "lit-html/directives/style-map"; | import { styleMap } from "lit-html/directives/style-map"; | ||||||
|  |  | ||||||
| import type { HomeAssistant, WeatherEntity } from "../types"; | import type { HomeAssistant, WeatherEntity } from "../types"; | ||||||
| import { roundWithOneDecimal } from "../util/calculate"; |  | ||||||
|  |  | ||||||
| export const weatherSVGs = new Set<string>([ | export const weatherSVGs = new Set<string>([ | ||||||
|   "clear-night", |   "clear-night", | ||||||
| @@ -136,7 +135,7 @@ export const getSecondaryWeatherAttribute = ( | |||||||
|   return ` |   return ` | ||||||
|     ${hass!.localize( |     ${hass!.localize( | ||||||
|       `ui.card.weather.attributes.${attribute}` |       `ui.card.weather.attributes.${attribute}` | ||||||
|     )} ${roundWithOneDecimal(value)} ${getWeatherUnit(hass!, attribute)} |     )} ${value} ${getWeatherUnit(hass!, attribute)} | ||||||
|   `; |   `; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,22 +1,21 @@ | |||||||
| import "@material/mwc-button"; | import "@material/mwc-button"; | ||||||
| import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; | import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; | ||||||
|  | import "../../components/ha-icon-button"; | ||||||
|  | import "../../components/ha-circular-progress"; | ||||||
|  | import "@polymer/paper-tooltip/paper-tooltip"; | ||||||
| import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
|   CSSResultArray, |   CSSResultArray, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|  |   internalProperty, | ||||||
|   PropertyValues, |   PropertyValues, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; |  | ||||||
| import { computeRTL } from "../../common/util/compute_rtl"; |  | ||||||
| import "../../components/ha-circular-progress"; |  | ||||||
| import "../../components/ha-dialog"; | import "../../components/ha-dialog"; | ||||||
| import "../../components/ha-form/ha-form"; | import "../../components/ha-form/ha-form"; | ||||||
| import "../../components/ha-icon-button"; |  | ||||||
| import "../../components/ha-markdown"; | import "../../components/ha-markdown"; | ||||||
| import { | import { | ||||||
|   AreaRegistryEntry, |   AreaRegistryEntry, | ||||||
| @@ -36,6 +35,8 @@ import "./step-flow-external"; | |||||||
| import "./step-flow-form"; | import "./step-flow-form"; | ||||||
| import "./step-flow-loading"; | import "./step-flow-loading"; | ||||||
| import "./step-flow-pick-handler"; | import "./step-flow-pick-handler"; | ||||||
|  | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
|  | import { computeRTL } from "../../common/util/compute_rtl"; | ||||||
|  |  | ||||||
| let instance = 0; | let instance = 0; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -97,13 +97,8 @@ export const showConfigFlowDialog = ( | |||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     renderExternalStepHeader(hass, step) { |     renderExternalStepHeader(hass, step) { | ||||||
|       return ( |       return hass.localize( | ||||||
|         hass.localize( |  | ||||||
|         `component.${step.handler}.config.step.${step.step_id}.title` |         `component.${step.handler}.config.step.${step.step_id}.title` | ||||||
|         ) || |  | ||||||
|         hass.localize( |  | ||||||
|           "ui.panel.config.integrations.config_flow.external_step.open_site" |  | ||||||
|         ) |  | ||||||
|       ); |       ); | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import "@material/mwc-button"; | import "@material/mwc-button"; | ||||||
|  | import "../../components/ha-circular-progress"; | ||||||
| import "@polymer/paper-tooltip/paper-tooltip"; | import "@polymer/paper-tooltip/paper-tooltip"; | ||||||
| import { | import { | ||||||
|   css, |   css, | ||||||
| @@ -11,7 +12,6 @@ import { | |||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import "../../components/ha-circular-progress"; |  | ||||||
| import "../../components/ha-form/ha-form"; | import "../../components/ha-form/ha-form"; | ||||||
| import type { HaFormSchema } from "../../components/ha-form/ha-form"; | import type { HaFormSchema } from "../../components/ha-form/ha-form"; | ||||||
| import "../../components/ha-markdown"; | import "../../components/ha-markdown"; | ||||||
| @@ -91,7 +91,7 @@ class StepFlowForm extends LitElement { | |||||||
|  |  | ||||||
|                 ${!allRequiredInfoFilledIn |                 ${!allRequiredInfoFilledIn | ||||||
|                   ? html` |                   ? html` | ||||||
|                       <paper-tooltip animation-delay="0" position="left" |                       <paper-tooltip position="left" | ||||||
|                         >${this.hass.localize( |                         >${this.hass.localize( | ||||||
|                           "ui.panel.config.integrations.config_flow.not_all_required_fields" |                           "ui.panel.config.integrations.config_flow.not_all_required_fields" | ||||||
|                         )} |                         )} | ||||||
|   | |||||||
| @@ -4,35 +4,27 @@ import { | |||||||
|   CSSResultArray, |   CSSResultArray, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|  |   internalProperty, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import "../../components/dialog/ha-paper-dialog"; | ||||||
| import { createCloseHeading } from "../../components/ha-dialog"; |  | ||||||
| import "../../components/ha-formfield"; |  | ||||||
| import "../../components/ha-switch"; |  | ||||||
| import { domainToName } from "../../data/integration"; | import { domainToName } from "../../data/integration"; | ||||||
|  | import { PolymerChangedEvent } from "../../polymer-types"; | ||||||
| import { haStyleDialog } from "../../resources/styles"; | import { haStyleDialog } from "../../resources/styles"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../types"; | ||||||
| import { HassDialog } from "../make-dialog-manager"; |  | ||||||
| import { HaDomainTogglerDialogParams } from "./show-dialog-domain-toggler"; | import { HaDomainTogglerDialogParams } from "./show-dialog-domain-toggler"; | ||||||
|  |  | ||||||
| @customElement("dialog-domain-toggler") | @customElement("dialog-domain-toggler") | ||||||
| class DomainTogglerDialog extends LitElement implements HassDialog { | class DomainTogglerDialog extends LitElement { | ||||||
|   public hass!: HomeAssistant; |   public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|   @internalProperty() private _params?: HaDomainTogglerDialogParams; |   @internalProperty() private _params?: HaDomainTogglerDialogParams; | ||||||
|  |  | ||||||
|   public showDialog(params: HaDomainTogglerDialogParams): void { |   public async showDialog(params: HaDomainTogglerDialogParams): Promise<void> { | ||||||
|     this._params = params; |     this._params = params; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public closeDialog() { |  | ||||||
|     this._params = undefined; |  | ||||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     if (!this._params) { |     if (!this._params) { | ||||||
|       return html``; |       return html``; | ||||||
| @@ -43,49 +35,46 @@ class DomainTogglerDialog extends LitElement implements HassDialog { | |||||||
|       .sort(); |       .sort(); | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-dialog |       <ha-paper-dialog | ||||||
|         open |         with-backdrop | ||||||
|         @closed=${this.closeDialog} |         opened | ||||||
|         scrimClickAction |         @opened-changed=${this._openedChanged} | ||||||
|         escapeKeyAction |  | ||||||
|         hideActions |  | ||||||
|         .heading=${createCloseHeading( |  | ||||||
|           this.hass, |  | ||||||
|           this.hass.localize("ui.dialogs.domain_toggler.title") |  | ||||||
|         )} |  | ||||||
|       > |       > | ||||||
|  |         <h2> | ||||||
|  |           ${this.hass.localize("ui.dialogs.domain_toggler.title")} | ||||||
|  |         </h2> | ||||||
|         <div> |         <div> | ||||||
|           ${domains.map( |           ${domains.map( | ||||||
|             (domain) => |             (domain) => | ||||||
|               html` |               html` | ||||||
|                 <ha-formfield .label=${domain[0]}> |                 <div>${domain[0]}</div> | ||||||
|                   <ha-switch |                 <mwc-button .domain=${domain[1]} @click=${this._handleOff}> | ||||||
|                     .domain=${domain[1]} |                   ${this.hass.localize("state.default.off")} | ||||||
|                     .checked=${!this._params!.exposedDomains || |                 </mwc-button> | ||||||
|                     this._params!.exposedDomains.includes(domain[1])} |                 <mwc-button .domain=${domain[1]} @click=${this._handleOn}> | ||||||
|                     @change=${this._handleSwitch} |                   ${this.hass.localize("state.default.on")} | ||||||
|                   > |  | ||||||
|                   </ha-switch> |  | ||||||
|                 </ha-formfield> |  | ||||||
|                 <mwc-button .domain=${domain[1]} @click=${this._handleReset}> |  | ||||||
|                   ${this.hass.localize( |  | ||||||
|                     "ui.dialogs.domain_toggler.reset_entities" |  | ||||||
|                   )} |  | ||||||
|                 </mwc-button> |                 </mwc-button> | ||||||
|               ` |               ` | ||||||
|           )} |           )} | ||||||
|         </div> |         </div> | ||||||
|       </ha-dialog> |       </ha-paper-dialog> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _handleSwitch(ev) { |   private _openedChanged(ev: PolymerChangedEvent<boolean>): void { | ||||||
|     this._params!.toggleDomain(ev.currentTarget.domain, ev.target.checked); |     // Closed dialog by clicking on the overlay | ||||||
|  |     if (!ev.detail.value) { | ||||||
|  |       this._params = undefined; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _handleOff(ev) { | ||||||
|  |     this._params!.toggleDomain(ev.currentTarget.domain, false); | ||||||
|     ev.currentTarget.blur(); |     ev.currentTarget.blur(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _handleReset(ev) { |   private _handleOn(ev) { | ||||||
|     this._params!.resetDomain(ev.currentTarget.domain); |     this._params!.toggleDomain(ev.currentTarget.domain, true); | ||||||
|     ev.currentTarget.blur(); |     ev.currentTarget.blur(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -93,13 +82,12 @@ class DomainTogglerDialog extends LitElement implements HassDialog { | |||||||
|     return [ |     return [ | ||||||
|       haStyleDialog, |       haStyleDialog, | ||||||
|       css` |       css` | ||||||
|         ha-dialog { |         ha-paper-dialog { | ||||||
|           --mdc-dialog-max-width: 500px; |           max-width: 500px; | ||||||
|         } |         } | ||||||
|         div { |         div { | ||||||
|           display: grid; |           display: grid; | ||||||
|           grid-template-columns: auto auto; |           grid-template-columns: auto auto auto; | ||||||
|           grid-row-gap: 8px; |  | ||||||
|           align-items: center; |           align-items: center; | ||||||
|         } |         } | ||||||
|       `, |       `, | ||||||
|   | |||||||
| @@ -2,9 +2,7 @@ import { fireEvent } from "../../common/dom/fire_event"; | |||||||
|  |  | ||||||
| export interface HaDomainTogglerDialogParams { | export interface HaDomainTogglerDialogParams { | ||||||
|   domains: string[]; |   domains: string[]; | ||||||
|   exposedDomains: string[] | null; |  | ||||||
|   toggleDomain: (domain: string, turnOn: boolean) => void; |   toggleDomain: (domain: string, turnOn: boolean) => void; | ||||||
|   resetDomain: (domain: string) => void; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export const loadDomainTogglerDialog = () => | export const loadDomainTogglerDialog = () => | ||||||
|   | |||||||
| @@ -5,19 +5,19 @@ import { | |||||||
|   CSSResult, |   CSSResult, | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| import { classMap } from "lit-html/directives/class-map"; | import { classMap } from "lit-html/directives/class-map"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; |  | ||||||
| import "../../components/ha-dialog"; | import "../../components/ha-dialog"; | ||||||
| import "../../components/ha-switch"; | import "../../components/ha-switch"; | ||||||
| import { PolymerChangedEvent } from "../../polymer-types"; | import { PolymerChangedEvent } from "../../polymer-types"; | ||||||
| import { haStyleDialog } from "../../resources/styles"; | import { haStyleDialog } from "../../resources/styles"; | ||||||
| import { HomeAssistant } from "../../types"; | import { HomeAssistant } from "../../types"; | ||||||
| import { DialogParams } from "./show-dialog-box"; | import { DialogParams } from "./show-dialog-box"; | ||||||
|  | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
|  |  | ||||||
| @customElement("dialog-box") | @customElement("dialog-box") | ||||||
| class DialogBox extends LitElement { | class DialogBox extends LitElement { | ||||||
| @@ -55,9 +55,9 @@ class DialogBox extends LitElement { | |||||||
|     return html` |     return html` | ||||||
|       <ha-dialog |       <ha-dialog | ||||||
|         open |         open | ||||||
|         ?scrimClickAction=${this._params.prompt} |         scrimClickAction | ||||||
|         ?escapeKeyAction=${this._params.prompt} |         escapeKeyAction | ||||||
|         @closed=${this._dismiss} |         @close=${this._close} | ||||||
|         .heading=${this._params.title |         .heading=${this._params.title | ||||||
|           ? this._params.title |           ? this._params.title | ||||||
|           : this._params.confirmation && |           : this._params.confirmation && | ||||||
| @@ -114,8 +114,8 @@ class DialogBox extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _dismiss(): void { |   private _dismiss(): void { | ||||||
|     if (this._params?.cancel) { |     if (this._params!.cancel) { | ||||||
|       this._params.cancel(); |       this._params!.cancel(); | ||||||
|     } |     } | ||||||
|     this._close(); |     this._close(); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,136 +0,0 @@ | |||||||
| import "@material/mwc-button/mwc-button"; |  | ||||||
| import Cropper from "cropperjs"; |  | ||||||
| // @ts-ignore |  | ||||||
| import cropperCss from "cropperjs/dist/cropper.css"; |  | ||||||
| import { |  | ||||||
|   css, |  | ||||||
|   CSSResult, |  | ||||||
|   customElement, |  | ||||||
|   html, |  | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |  | ||||||
|   property, |  | ||||||
|   PropertyValues, |  | ||||||
|   query, |  | ||||||
|   TemplateResult, |  | ||||||
|   unsafeCSS, |  | ||||||
| } from "lit-element"; |  | ||||||
| import "../../components/ha-dialog"; |  | ||||||
| import { haStyleDialog } from "../../resources/styles"; |  | ||||||
| import type { HomeAssistant } from "../../types"; |  | ||||||
| import { HaImageCropperDialogParams } from "./show-image-cropper-dialog"; |  | ||||||
| import { classMap } from "lit-html/directives/class-map"; |  | ||||||
|  |  | ||||||
| @customElement("image-cropper-dialog") |  | ||||||
| export class HaImagecropperDialog extends LitElement { |  | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _params?: HaImageCropperDialogParams; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _open = false; |  | ||||||
|  |  | ||||||
|   @query("img") private _image!: HTMLImageElement; |  | ||||||
|  |  | ||||||
|   private _cropper?: Cropper; |  | ||||||
|  |  | ||||||
|   public showDialog(params: HaImageCropperDialogParams): void { |  | ||||||
|     this._params = params; |  | ||||||
|     this._open = true; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public closeDialog() { |  | ||||||
|     this._open = false; |  | ||||||
|     this._params = undefined; |  | ||||||
|     this._cropper?.destroy(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected updated(changedProperties: PropertyValues) { |  | ||||||
|     if (!changedProperties.has("_params") || !this._params) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     if (!this._cropper) { |  | ||||||
|       this._image.src = URL.createObjectURL(this._params.file); |  | ||||||
|       this._cropper = new Cropper(this._image, { |  | ||||||
|         aspectRatio: this._params.options.aspectRatio, |  | ||||||
|         viewMode: 1, |  | ||||||
|         dragMode: "move", |  | ||||||
|         minCropBoxWidth: 50, |  | ||||||
|         ready: () => { |  | ||||||
|           URL.revokeObjectURL(this._image!.src); |  | ||||||
|         }, |  | ||||||
|       }); |  | ||||||
|     } else { |  | ||||||
|       this._cropper.replace(URL.createObjectURL(this._params.file)); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |  | ||||||
|     return html`<ha-dialog |  | ||||||
|       @closed=${this.closeDialog} |  | ||||||
|       scrimClickAction |  | ||||||
|       escapeKeyAction |  | ||||||
|       .open=${this._open} |  | ||||||
|     > |  | ||||||
|       <div |  | ||||||
|         class="container ${classMap({ |  | ||||||
|           round: Boolean(this._params?.options.round), |  | ||||||
|         })}" |  | ||||||
|       > |  | ||||||
|         <img /> |  | ||||||
|       </div> |  | ||||||
|       <mwc-button slot="secondaryAction" @click=${this.closeDialog}> |  | ||||||
|         ${this.hass.localize("ui.common.cancel")} |  | ||||||
|       </mwc-button> |  | ||||||
|       <mwc-button slot="primaryAction" @click=${this._cropImage}> |  | ||||||
|         ${this.hass.localize("ui.dialogs.image_cropper.crop")} |  | ||||||
|       </mwc-button> |  | ||||||
|     </ha-dialog>`; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _cropImage() { |  | ||||||
|     this._cropper!.getCroppedCanvas().toBlob( |  | ||||||
|       (blob) => { |  | ||||||
|         if (!blob) { |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         const file = new File([blob], this._params!.file.name, { |  | ||||||
|           type: this._params!.options.type || this._params!.file.type, |  | ||||||
|         }); |  | ||||||
|         this._params!.croppedCallback(file); |  | ||||||
|         this.closeDialog(); |  | ||||||
|       }, |  | ||||||
|       this._params!.options.type || this._params!.file.type, |  | ||||||
|       this._params!.options.quality |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult[] { |  | ||||||
|     return [ |  | ||||||
|       haStyleDialog, |  | ||||||
|       css` |  | ||||||
|         ${unsafeCSS(cropperCss)} |  | ||||||
|         .container { |  | ||||||
|           max-width: 640px; |  | ||||||
|         } |  | ||||||
|         img { |  | ||||||
|           max-width: 100%; |  | ||||||
|         } |  | ||||||
|         .container.round .cropper-view-box, |  | ||||||
|         .container.round .cropper-face { |  | ||||||
|           border-radius: 50%; |  | ||||||
|         } |  | ||||||
|         .cropper-line, |  | ||||||
|         .cropper-point, |  | ||||||
|         .cropper-point.point-se::before { |  | ||||||
|           background-color: var(--primary-color); |  | ||||||
|         } |  | ||||||
|       `, |  | ||||||
|     ]; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "image-cropper-dialog": HaImagecropperDialog; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,30 +0,0 @@ | |||||||
| import { fireEvent } from "../../common/dom/fire_event"; |  | ||||||
|  |  | ||||||
| export interface CropOptions { |  | ||||||
|   round: boolean; |  | ||||||
|   type?: "image/jpeg" | "image/png"; |  | ||||||
|   quality?: number; |  | ||||||
|   aspectRatio: number; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface HaImageCropperDialogParams { |  | ||||||
|   file: File; |  | ||||||
|   options: CropOptions; |  | ||||||
|   croppedCallback: (file: File) => void; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const loadImageCropperDialog = () => |  | ||||||
|   import( |  | ||||||
|     /* webpackChunkName: "image-cropper-dialog" */ "./image-cropper-dialog" |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
| export const showImageCropperDialog = ( |  | ||||||
|   element: HTMLElement, |  | ||||||
|   dialogParams: HaImageCropperDialogParams |  | ||||||
| ): void => { |  | ||||||
|   fireEvent(element, "show-dialog", { |  | ||||||
|     dialogTag: "image-cropper-dialog", |  | ||||||
|     dialogImport: loadImageCropperDialog, |  | ||||||
|     dialogParams, |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| @@ -34,10 +34,7 @@ class MoreInfoAutomation extends LitElement { | |||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div class="actions"> |       <div class="actions"> | ||||||
|         <mwc-button |         <mwc-button @click=${this.handleAction}> | ||||||
|           @click=${this.handleAction} |  | ||||||
|           .disabled=${this.stateObj!.state === "unavailable"} |  | ||||||
|         > |  | ||||||
|           ${this.hass.localize("ui.card.automation.trigger")} |           ${this.hass.localize("ui.card.automation.trigger")} | ||||||
|         </mwc-button> |         </mwc-button> | ||||||
|       </div> |       </div> | ||||||
| @@ -55,7 +52,7 @@ class MoreInfoAutomation extends LitElement { | |||||||
|         justify-content: space-between; |         justify-content: space-between; | ||||||
|       } |       } | ||||||
|       .actions { |       .actions { | ||||||
|         margin: 8px 0; |         margin: 36px 0 8px 0; | ||||||
|         text-align: right; |         text-align: right; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -4,9 +4,9 @@ import { | |||||||
|   css, |   css, | ||||||
|   CSSResult, |   CSSResult, | ||||||
|   html, |   html, | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |   LitElement, | ||||||
|   property, |   property, | ||||||
|  |   internalProperty, | ||||||
|   PropertyValues, |   PropertyValues, | ||||||
|   TemplateResult, |   TemplateResult, | ||||||
| } from "lit-element"; | } from "lit-element"; | ||||||
| @@ -47,7 +47,7 @@ class MoreInfoCamera extends LitElement { | |||||||
|     return html` |     return html` | ||||||
|       <ha-camera-stream |       <ha-camera-stream | ||||||
|         .hass=${this.hass} |         .hass=${this.hass} | ||||||
|         .stateObj=${this.stateObj} |         .stateObj="${this.stateObj}" | ||||||
|         showcontrols |         showcontrols | ||||||
|       ></ha-camera-stream> |       ></ha-camera-stream> | ||||||
|       ${this._cameraPrefs |       ${this._cameraPrefs | ||||||
|   | |||||||
							
								
								
									
										361
									
								
								src/dialogs/more-info/controls/more-info-light.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										361
									
								
								src/dialogs/more-info/controls/more-info-light.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,361 @@ | |||||||
|  | import "@polymer/iron-flex-layout/iron-flex-layout-classes"; | ||||||
|  | import "@polymer/paper-item/paper-item"; | ||||||
|  | import "@polymer/paper-listbox/paper-listbox"; | ||||||
|  | import { html } from "@polymer/polymer/lib/utils/html-tag"; | ||||||
|  | /* eslint-plugin-disable lit */ | ||||||
|  | import { PolymerElement } from "@polymer/polymer/polymer-element"; | ||||||
|  | import { featureClassNames } from "../../../common/entity/feature_class_names"; | ||||||
|  | import "../../../components/ha-attributes"; | ||||||
|  | import "../../../components/ha-color-picker"; | ||||||
|  | import "../../../components/ha-labeled-slider"; | ||||||
|  | import "../../../components/ha-paper-dropdown-menu"; | ||||||
|  | import { EventsMixin } from "../../../mixins/events-mixin"; | ||||||
|  | import LocalizeMixin from "../../../mixins/localize-mixin"; | ||||||
|  | import "../../../components/ha-icon-button"; | ||||||
|  |  | ||||||
|  | const FEATURE_CLASS_NAMES = { | ||||||
|  |   1: "has-brightness", | ||||||
|  |   2: "has-color_temp", | ||||||
|  |   4: "has-effect_list", | ||||||
|  |   16: "has-color", | ||||||
|  |   128: "has-white_value", | ||||||
|  | }; | ||||||
|  | /* | ||||||
|  |  * @appliesMixin EventsMixin | ||||||
|  |  */ | ||||||
|  | class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) { | ||||||
|  |   static get template() { | ||||||
|  |     return html` | ||||||
|  |       <style include="iron-flex"></style> | ||||||
|  |       <style> | ||||||
|  |         .content { | ||||||
|  |           display: flex; | ||||||
|  |           flex-direction: column; | ||||||
|  |           align-items: center; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .effect_list, | ||||||
|  |         .brightness, | ||||||
|  |         .color_temp, | ||||||
|  |         .white_value { | ||||||
|  |           max-height: 0px; | ||||||
|  |           overflow: hidden; | ||||||
|  |           transition: max-height 0.5s ease-in; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .color_temp { | ||||||
|  |           --ha-slider-background: -webkit-linear-gradient( | ||||||
|  |             right, | ||||||
|  |             rgb(255, 160, 0) 0%, | ||||||
|  |             white 50%, | ||||||
|  |             rgb(166, 209, 255) 100% | ||||||
|  |           ); | ||||||
|  |           /* The color temp minimum value shouldn't be rendered differently. It's not "off". */ | ||||||
|  |           --paper-slider-knob-start-border-color: var(--primary-color); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .segmentationContainer { | ||||||
|  |           position: relative; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ha-color-picker { | ||||||
|  |           display: block; | ||||||
|  |           width: 100%; | ||||||
|  |  | ||||||
|  |           max-height: 0px; | ||||||
|  |           overflow: hidden; | ||||||
|  |           transition: max-height 0.5s ease-in; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .segmentationButton { | ||||||
|  |           display: none; | ||||||
|  |           position: absolute; | ||||||
|  |           top: 5%; | ||||||
|  |           transform: translate(0%, 0%); | ||||||
|  |           color: var(--secondary-text-color); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .has-color.is-on .segmentationButton { | ||||||
|  |           display: inline-block; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .has-effect_list.is-on .effect_list, | ||||||
|  |         .has-brightness .brightness, | ||||||
|  |         .has-color_temp.is-on .color_temp, | ||||||
|  |         .has-white_value.is-on .white_value { | ||||||
|  |           max-height: 84px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .has-brightness .has-color_temp.is-on, | ||||||
|  |         .has-white_value.is-on { | ||||||
|  |           margin-top: -16px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .has-brightness .brightness, | ||||||
|  |         .has-color_temp.is-on .color_temp, | ||||||
|  |         .has-white_value.is-on .white_value { | ||||||
|  |           padding-top: 16px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .has-color.is-on ha-color-picker { | ||||||
|  |           max-height: 500px; | ||||||
|  |           overflow: visible; | ||||||
|  |           --ha-color-picker-wheel-borderwidth: 5; | ||||||
|  |           --ha-color-picker-wheel-bordercolor: white; | ||||||
|  |           --ha-color-picker-wheel-shadow: none; | ||||||
|  |           --ha-color-picker-marker-borderwidth: 2; | ||||||
|  |           --ha-color-picker-marker-bordercolor: white; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .control { | ||||||
|  |           width: 100%; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .is-unavailable .control { | ||||||
|  |           max-height: 0px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ha-attributes { | ||||||
|  |           width: 100%; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ha-paper-dropdown-menu { | ||||||
|  |           width: 100%; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         paper-item { | ||||||
|  |           cursor: pointer; | ||||||
|  |         } | ||||||
|  |       </style> | ||||||
|  |  | ||||||
|  |       <div class$="[[computeClassNames(stateObj)]]"> | ||||||
|  |         <div class="control brightness"> | ||||||
|  |           <ha-labeled-slider | ||||||
|  |             caption="[[localize('ui.card.light.brightness')]]" | ||||||
|  |             icon="hass:brightness-5" | ||||||
|  |             min="1" | ||||||
|  |             max="255" | ||||||
|  |             value="{{brightnessSliderValue}}" | ||||||
|  |             on-change="brightnessSliderChanged" | ||||||
|  |           ></ha-labeled-slider> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div class="control color_temp"> | ||||||
|  |           <ha-labeled-slider | ||||||
|  |             caption="[[localize('ui.card.light.color_temperature')]]" | ||||||
|  |             icon="hass:thermometer" | ||||||
|  |             min="[[stateObj.attributes.min_mireds]]" | ||||||
|  |             max="[[stateObj.attributes.max_mireds]]" | ||||||
|  |             value="{{ctSliderValue}}" | ||||||
|  |             on-change="ctSliderChanged" | ||||||
|  |           ></ha-labeled-slider> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div class="control white_value"> | ||||||
|  |           <ha-labeled-slider | ||||||
|  |             caption="[[localize('ui.card.light.white_value')]]" | ||||||
|  |             icon="hass:file-word-box" | ||||||
|  |             max="255" | ||||||
|  |             value="{{wvSliderValue}}" | ||||||
|  |             on-change="wvSliderChanged" | ||||||
|  |           ></ha-labeled-slider> | ||||||
|  |         </div> | ||||||
|  |         <div class="segmentationContainer"> | ||||||
|  |           <ha-color-picker | ||||||
|  |             class="control color" | ||||||
|  |             on-colorselected="colorPicked" | ||||||
|  |             desired-hs-color="{{colorPickerColor}}" | ||||||
|  |             throttle="500" | ||||||
|  |             hue-segments="{{hueSegments}}" | ||||||
|  |             saturation-segments="{{saturationSegments}}" | ||||||
|  |           > | ||||||
|  |           </ha-color-picker> | ||||||
|  |           <ha-icon-button | ||||||
|  |             icon="mdi:palette" | ||||||
|  |             on-click="segmentClick" | ||||||
|  |             class="segmentationButton" | ||||||
|  |           ></ha-icon-button> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div class="control effect_list"> | ||||||
|  |           <ha-paper-dropdown-menu | ||||||
|  |             label-float="" | ||||||
|  |             dynamic-align="" | ||||||
|  |             label="[[localize('ui.card.light.effect')]]" | ||||||
|  |           > | ||||||
|  |             <paper-listbox | ||||||
|  |               slot="dropdown-content" | ||||||
|  |               selected="[[stateObj.attributes.effect]]" | ||||||
|  |               on-selected-changed="effectChanged" | ||||||
|  |               attr-for-selected="item-name" | ||||||
|  |             > | ||||||
|  |               <template | ||||||
|  |                 is="dom-repeat" | ||||||
|  |                 items="[[stateObj.attributes.effect_list]]" | ||||||
|  |               > | ||||||
|  |                 <paper-item item-name$="[[item]]">[[item]]</paper-item> | ||||||
|  |               </template> | ||||||
|  |             </paper-listbox> | ||||||
|  |           </ha-paper-dropdown-menu> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <ha-attributes | ||||||
|  |           state-obj="[[stateObj]]" | ||||||
|  |           extra-filters="brightness,color_temp,white_value,effect_list,effect,hs_color,rgb_color,xy_color,min_mireds,max_mireds,entity_id" | ||||||
|  |         ></ha-attributes> | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static get properties() { | ||||||
|  |     return { | ||||||
|  |       hass: { | ||||||
|  |         type: Object, | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       stateObj: { | ||||||
|  |         type: Object, | ||||||
|  |         observer: "stateObjChanged", | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       brightnessSliderValue: { | ||||||
|  |         type: Number, | ||||||
|  |         value: 0, | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       ctSliderValue: { | ||||||
|  |         type: Number, | ||||||
|  |         value: 0, | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       wvSliderValue: { | ||||||
|  |         type: Number, | ||||||
|  |         value: 0, | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       hueSegments: { | ||||||
|  |         type: Number, | ||||||
|  |         value: 24, | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       saturationSegments: { | ||||||
|  |         type: Number, | ||||||
|  |         value: 8, | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       colorPickerColor: { | ||||||
|  |         type: Object, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   stateObjChanged(newVal, oldVal) { | ||||||
|  |     const props = { | ||||||
|  |       brightnessSliderValue: 0, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if (newVal && newVal.state === "on") { | ||||||
|  |       props.brightnessSliderValue = newVal.attributes.brightness; | ||||||
|  |       props.ctSliderValue = newVal.attributes.color_temp; | ||||||
|  |       props.wvSliderValue = newVal.attributes.white_value; | ||||||
|  |       if (newVal.attributes.hs_color) { | ||||||
|  |         props.colorPickerColor = { | ||||||
|  |           h: newVal.attributes.hs_color[0], | ||||||
|  |           s: newVal.attributes.hs_color[1] / 100, | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.setProperties(props); | ||||||
|  |  | ||||||
|  |     if (oldVal) { | ||||||
|  |       setTimeout(() => { | ||||||
|  |         this.fire("iron-resize"); | ||||||
|  |       }, 500); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   computeClassNames(stateObj) { | ||||||
|  |     const classes = [ | ||||||
|  |       "content", | ||||||
|  |       featureClassNames(stateObj, FEATURE_CLASS_NAMES), | ||||||
|  |     ]; | ||||||
|  |     if (stateObj && stateObj.state === "on") { | ||||||
|  |       classes.push("is-on"); | ||||||
|  |     } | ||||||
|  |     if (stateObj && stateObj.state === "unavailable") { | ||||||
|  |       classes.push("is-unavailable"); | ||||||
|  |     } | ||||||
|  |     return classes.join(" "); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   effectChanged(ev) { | ||||||
|  |     const oldVal = this.stateObj.attributes.effect; | ||||||
|  |     const newVal = ev.detail.value; | ||||||
|  |  | ||||||
|  |     if (!newVal || oldVal === newVal) return; | ||||||
|  |  | ||||||
|  |     this.hass.callService("light", "turn_on", { | ||||||
|  |       entity_id: this.stateObj.entity_id, | ||||||
|  |       effect: newVal, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   brightnessSliderChanged(ev) { | ||||||
|  |     const bri = parseInt(ev.target.value, 10); | ||||||
|  |  | ||||||
|  |     if (isNaN(bri)) return; | ||||||
|  |  | ||||||
|  |     this.hass.callService("light", "turn_on", { | ||||||
|  |       entity_id: this.stateObj.entity_id, | ||||||
|  |       brightness: bri, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ctSliderChanged(ev) { | ||||||
|  |     const ct = parseInt(ev.target.value, 10); | ||||||
|  |  | ||||||
|  |     if (isNaN(ct)) return; | ||||||
|  |  | ||||||
|  |     this.hass.callService("light", "turn_on", { | ||||||
|  |       entity_id: this.stateObj.entity_id, | ||||||
|  |       color_temp: ct, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   wvSliderChanged(ev) { | ||||||
|  |     const wv = parseInt(ev.target.value, 10); | ||||||
|  |  | ||||||
|  |     if (isNaN(wv)) return; | ||||||
|  |  | ||||||
|  |     this.hass.callService("light", "turn_on", { | ||||||
|  |       entity_id: this.stateObj.entity_id, | ||||||
|  |       white_value: wv, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   segmentClick() { | ||||||
|  |     if (this.hueSegments === 24 && this.saturationSegments === 8) { | ||||||
|  |       this.setProperties({ hueSegments: 0, saturationSegments: 0 }); | ||||||
|  |     } else { | ||||||
|  |       this.setProperties({ hueSegments: 24, saturationSegments: 8 }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   serviceChangeColor(hass, entityId, color) { | ||||||
|  |     hass.callService("light", "turn_on", { | ||||||
|  |       entity_id: entityId, | ||||||
|  |       hs_color: [color.h, color.s * 100], | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Called when a new color has been picked. | ||||||
|  |    * should be throttled with the 'throttle=' attribute of the color picker | ||||||
|  |    */ | ||||||
|  |   colorPicked(ev) { | ||||||
|  |     this.serviceChangeColor(this.hass, this.stateObj.entity_id, ev.detail.hs); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | customElements.define("more-info-light", MoreInfoLight); | ||||||
| @@ -1,305 +0,0 @@ | |||||||
| import "@polymer/paper-item/paper-item"; |  | ||||||
| import "@polymer/paper-listbox/paper-listbox"; |  | ||||||
| import { |  | ||||||
|   css, |  | ||||||
|   CSSResult, |  | ||||||
|   customElement, |  | ||||||
|   html, |  | ||||||
|   internalProperty, |  | ||||||
|   LitElement, |  | ||||||
|   property, |  | ||||||
|   PropertyValues, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit-element"; |  | ||||||
| import { classMap } from "lit-html/directives/class-map"; |  | ||||||
| import { supportsFeature } from "../../../common/entity/supports-feature"; |  | ||||||
| import "../../../components/ha-attributes"; |  | ||||||
| import "../../../components/ha-color-picker"; |  | ||||||
| import "../../../components/ha-icon-button"; |  | ||||||
| import "../../../components/ha-labeled-slider"; |  | ||||||
| import "../../../components/ha-paper-dropdown-menu"; |  | ||||||
| import { |  | ||||||
|   SUPPORT_BRIGHTNESS, |  | ||||||
|   SUPPORT_COLOR, |  | ||||||
|   SUPPORT_COLOR_TEMP, |  | ||||||
|   SUPPORT_EFFECT, |  | ||||||
|   SUPPORT_WHITE_VALUE, |  | ||||||
| } from "../../../data/light"; |  | ||||||
| import type { HomeAssistant, LightEntity } from "../../../types"; |  | ||||||
|  |  | ||||||
| interface HueSatColor { |  | ||||||
|   h: number; |  | ||||||
|   s: number; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @customElement("more-info-light") |  | ||||||
| class MoreInfoLight extends LitElement { |  | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   @property({ attribute: false }) public stateObj?: LightEntity; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _brightnessSliderValue = 0; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _ctSliderValue = 0; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _wvSliderValue = 0; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _hueSegments = 24; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _saturationSegments = 8; |  | ||||||
|  |  | ||||||
|   @internalProperty() private _colorPickerColor?: HueSatColor; |  | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |  | ||||||
|     if (!this.hass || !this.stateObj) { |  | ||||||
|       return html``; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return html` |  | ||||||
|       <div |  | ||||||
|         class="content ${classMap({ |  | ||||||
|           "is-on": this.stateObj.state === "on", |  | ||||||
|         })}" |  | ||||||
|       > |  | ||||||
|         ${this.stateObj.state === "on" |  | ||||||
|           ? html` |  | ||||||
|               ${supportsFeature(this.stateObj!, SUPPORT_BRIGHTNESS) |  | ||||||
|                 ? html` |  | ||||||
|                     <ha-labeled-slider |  | ||||||
|                       caption=${this.hass.localize("ui.card.light.brightness")} |  | ||||||
|                       icon="hass:brightness-5" |  | ||||||
|                       min="1" |  | ||||||
|                       max="255" |  | ||||||
|                       value=${this._brightnessSliderValue} |  | ||||||
|                       @change=${this._brightnessSliderChanged} |  | ||||||
|                     ></ha-labeled-slider> |  | ||||||
|                   ` |  | ||||||
|                 : ""} |  | ||||||
|               ${supportsFeature(this.stateObj, SUPPORT_COLOR_TEMP) |  | ||||||
|                 ? html` |  | ||||||
|                     <ha-labeled-slider |  | ||||||
|                       class="color_temp" |  | ||||||
|                       caption=${this.hass.localize( |  | ||||||
|                         "ui.card.light.color_temperature" |  | ||||||
|                       )} |  | ||||||
|                       icon="hass:thermometer" |  | ||||||
|                       .min=${this.stateObj.attributes.min_mireds} |  | ||||||
|                       .max=${this.stateObj.attributes.max_mireds} |  | ||||||
|                       .value=${this._ctSliderValue} |  | ||||||
|                       @change=${this._ctSliderChanged} |  | ||||||
|                     ></ha-labeled-slider> |  | ||||||
|                   ` |  | ||||||
|                 : ""} |  | ||||||
|               ${supportsFeature(this.stateObj, SUPPORT_WHITE_VALUE) |  | ||||||
|                 ? html` |  | ||||||
|                     <ha-labeled-slider |  | ||||||
|                       caption=${this.hass.localize("ui.card.light.white_value")} |  | ||||||
|                       icon="hass:file-word-box" |  | ||||||
|                       max="255" |  | ||||||
|                       .value=${this._wvSliderValue} |  | ||||||
|                       @change=${this._wvSliderChanged} |  | ||||||
|                     ></ha-labeled-slider> |  | ||||||
|                   ` |  | ||||||
|                 : ""} |  | ||||||
|               ${supportsFeature(this.stateObj, SUPPORT_COLOR) |  | ||||||
|                 ? html` |  | ||||||
|                     <div class="segmentationContainer"> |  | ||||||
|                       <ha-color-picker |  | ||||||
|                         class="color" |  | ||||||
|                         @colorselected=${this._colorPicked} |  | ||||||
|                         .desiredHsColor=${this._colorPickerColor} |  | ||||||
|                         throttle="500" |  | ||||||
|                         .hueSegments=${this._hueSegments} |  | ||||||
|                         .saturationSegments=${this._saturationSegments} |  | ||||||
|                       > |  | ||||||
|                       </ha-color-picker> |  | ||||||
|                       <ha-icon-button |  | ||||||
|                         icon="hass:palette" |  | ||||||
|                         @click=${this._segmentClick} |  | ||||||
|                         class="segmentationButton" |  | ||||||
|                       ></ha-icon-button> |  | ||||||
|                     </div> |  | ||||||
|                   ` |  | ||||||
|                 : ""} |  | ||||||
|               ${supportsFeature(this.stateObj, SUPPORT_EFFECT) && |  | ||||||
|               this.stateObj!.attributes.effect_list?.length |  | ||||||
|                 ? html` |  | ||||||
|                     <ha-paper-dropdown-menu |  | ||||||
|                       .label=${this.hass.localize("ui.card.light.effect")} |  | ||||||
|                     > |  | ||||||
|                       <paper-listbox |  | ||||||
|                         slot="dropdown-content" |  | ||||||
|                         .selected=${this.stateObj.attributes.effect || ""} |  | ||||||
|                         @iron-select=${this._effectChanged} |  | ||||||
|                         attr-for-selected="item-name" |  | ||||||
|                         >${this.stateObj.attributes.effect_list.map( |  | ||||||
|                           (effect: string) => html` |  | ||||||
|                             <paper-item .itemName=${effect} |  | ||||||
|                               >${effect}</paper-item |  | ||||||
|                             > |  | ||||||
|                           ` |  | ||||||
|                         )} |  | ||||||
|                       </paper-listbox> |  | ||||||
|                     </ha-paper-dropdown-menu> |  | ||||||
|                   ` |  | ||||||
|                 : ""} |  | ||||||
|             ` |  | ||||||
|           : ""} |  | ||||||
|         <ha-attributes |  | ||||||
|           .stateObj=${this.stateObj} |  | ||||||
|           extra-filters="brightness,color_temp,white_value,effect_list,effect,hs_color,rgb_color,xy_color,min_mireds,max_mireds,entity_id" |  | ||||||
|         ></ha-attributes> |  | ||||||
|       </div> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected updated(changedProps: PropertyValues): void { |  | ||||||
|     const stateObj = this.stateObj! as LightEntity; |  | ||||||
|     if (changedProps.has("stateObj") && stateObj.state === "on") { |  | ||||||
|       this._brightnessSliderValue = stateObj.attributes.brightness; |  | ||||||
|       this._ctSliderValue = stateObj.attributes.color_temp; |  | ||||||
|       this._wvSliderValue = stateObj.attributes.white_value; |  | ||||||
|  |  | ||||||
|       if (stateObj.attributes.hs_color) { |  | ||||||
|         this._colorPickerColor = { |  | ||||||
|           h: stateObj.attributes.hs_color[0], |  | ||||||
|           s: stateObj.attributes.hs_color[1] / 100, |  | ||||||
|         }; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _effectChanged(ev: CustomEvent) { |  | ||||||
|     const newVal = ev.detail.item.itemName; |  | ||||||
|  |  | ||||||
|     if (!newVal || this.stateObj!.attributes.effect === newVal) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.hass.callService("light", "turn_on", { |  | ||||||
|       entity_id: this.stateObj!.entity_id, |  | ||||||
|       effect: newVal, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _brightnessSliderChanged(ev: CustomEvent) { |  | ||||||
|     const bri = parseInt((ev.target as any).value, 10); |  | ||||||
|  |  | ||||||
|     if (isNaN(bri)) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.hass.callService("light", "turn_on", { |  | ||||||
|       entity_id: this.stateObj!.entity_id, |  | ||||||
|       brightness: bri, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _ctSliderChanged(ev: CustomEvent) { |  | ||||||
|     const ct = parseInt((ev.target as any).value, 10); |  | ||||||
|  |  | ||||||
|     if (isNaN(ct)) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.hass.callService("light", "turn_on", { |  | ||||||
|       entity_id: this.stateObj!.entity_id, |  | ||||||
|       color_temp: ct, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _wvSliderChanged(ev: CustomEvent) { |  | ||||||
|     const wv = parseInt((ev.target as any).value, 10); |  | ||||||
|  |  | ||||||
|     if (isNaN(wv)) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.hass.callService("light", "turn_on", { |  | ||||||
|       entity_id: this.stateObj!.entity_id, |  | ||||||
|       white_value: wv, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _segmentClick() { |  | ||||||
|     if (this._hueSegments === 24 && this._saturationSegments === 8) { |  | ||||||
|       this._hueSegments = 0; |  | ||||||
|       this._saturationSegments = 0; |  | ||||||
|     } else { |  | ||||||
|       this._hueSegments = 24; |  | ||||||
|       this._saturationSegments = 8; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Called when a new color has been picked. |  | ||||||
|    * should be throttled with the 'throttle=' attribute of the color picker |  | ||||||
|    */ |  | ||||||
|   private _colorPicked(ev: CustomEvent) { |  | ||||||
|     this.hass.callService("light", "turn_on", { |  | ||||||
|       entity_id: this.stateObj!.entity_id, |  | ||||||
|       hs_color: [ev.detail.hs.h, ev.detail.hs.s * 100], |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult { |  | ||||||
|     return css` |  | ||||||
|       .content { |  | ||||||
|         display: flex; |  | ||||||
|         flex-direction: column; |  | ||||||
|         align-items: center; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .content.is-on { |  | ||||||
|         margin-top: -16px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .content > * { |  | ||||||
|         width: 100%; |  | ||||||
|         max-height: 84px; |  | ||||||
|         overflow: hidden; |  | ||||||
|         padding-top: 16px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .color_temp { |  | ||||||
|         --ha-slider-background: -webkit-linear-gradient( |  | ||||||
|           right, |  | ||||||
|           rgb(255, 160, 0) 0%, |  | ||||||
|           white 50%, |  | ||||||
|           rgb(166, 209, 255) 100% |  | ||||||
|         ); |  | ||||||
|         /* The color temp minimum value shouldn't be rendered differently. It's not "off". */ |  | ||||||
|         --paper-slider-knob-start-border-color: var(--primary-color); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .segmentationContainer { |  | ||||||
|         position: relative; |  | ||||||
|         max-height: 500px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       ha-color-picker { |  | ||||||
|         --ha-color-picker-wheel-borderwidth: 5; |  | ||||||
|         --ha-color-picker-wheel-bordercolor: white; |  | ||||||
|         --ha-color-picker-wheel-shadow: none; |  | ||||||
|         --ha-color-picker-marker-borderwidth: 2; |  | ||||||
|         --ha-color-picker-marker-bordercolor: white; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .segmentationButton { |  | ||||||
|         position: absolute; |  | ||||||
|         top: 5%; |  | ||||||
|         color: var(--secondary-text-color); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       paper-item { |  | ||||||
|         cursor: pointer; |  | ||||||
|       } |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "more-info-light": MoreInfoLight; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										421
									
								
								src/dialogs/more-info/controls/more-info-media_player.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										421
									
								
								src/dialogs/more-info/controls/more-info-media_player.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,421 @@ | |||||||
|  | import "@polymer/iron-flex-layout/iron-flex-layout-classes"; | ||||||
|  | import "../../../components/ha-icon-button"; | ||||||
|  | import "@polymer/paper-item/paper-item"; | ||||||
|  | import "@polymer/paper-listbox/paper-listbox"; | ||||||
|  | import { html } from "@polymer/polymer/lib/utils/html-tag"; | ||||||
|  | /* eslint-plugin-disable lit */ | ||||||
|  | import { PolymerElement } from "@polymer/polymer/polymer-element"; | ||||||
|  | import { isComponentLoaded } from "../../../common/config/is_component_loaded"; | ||||||
|  | import { attributeClassNames } from "../../../common/entity/attribute_class_names"; | ||||||
|  | import { computeRTLDirection } from "../../../common/util/compute_rtl"; | ||||||
|  | import "../../../components/ha-paper-dropdown-menu"; | ||||||
|  | import "../../../components/ha-paper-slider"; | ||||||
|  | import "../../../components/ha-icon"; | ||||||
|  | import { EventsMixin } from "../../../mixins/events-mixin"; | ||||||
|  | import LocalizeMixin from "../../../mixins/localize-mixin"; | ||||||
|  | import HassMediaPlayerEntity from "../../../util/hass-media-player-model"; | ||||||
|  |  | ||||||
|  | /* | ||||||
|  |  * @appliesMixin LocalizeMixin | ||||||
|  |  * @appliesMixin EventsMixin | ||||||
|  |  */ | ||||||
|  | class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) { | ||||||
|  |   static get template() { | ||||||
|  |     return html` | ||||||
|  |       <style include="iron-flex iron-flex-alignment"></style> | ||||||
|  |       <style> | ||||||
|  |         .media-state { | ||||||
|  |           text-transform: capitalize; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ha-icon-button[highlight] { | ||||||
|  |           color: var(--accent-color); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .volume { | ||||||
|  |           margin-bottom: 8px; | ||||||
|  |  | ||||||
|  |           max-height: 0px; | ||||||
|  |           overflow: hidden; | ||||||
|  |           transition: max-height 0.5s ease-in; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .has-volume_level .volume { | ||||||
|  |           max-height: 40px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ha-icon.source-input { | ||||||
|  |           padding: 7px; | ||||||
|  |           margin-top: 15px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ha-paper-dropdown-menu.source-input { | ||||||
|  |           margin-left: 10px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         [hidden] { | ||||||
|  |           display: none !important; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         paper-item { | ||||||
|  |           cursor: pointer; | ||||||
|  |         } | ||||||
|  |       </style> | ||||||
|  |  | ||||||
|  |       <div class$="[[computeClassNames(stateObj)]]"> | ||||||
|  |         <div class="layout horizontal"> | ||||||
|  |           <div class="flex"> | ||||||
|  |             <ha-icon-button | ||||||
|  |               icon="hass:power" | ||||||
|  |               highlight$="[[playerObj.isOff]]" | ||||||
|  |               on-click="handleTogglePower" | ||||||
|  |               hidden$="[[computeHidePowerButton(playerObj)]]" | ||||||
|  |             ></ha-icon-button> | ||||||
|  |           </div> | ||||||
|  |           <div> | ||||||
|  |             <template | ||||||
|  |               is="dom-if" | ||||||
|  |               if="[[computeShowPlaybackControls(playerObj)]]" | ||||||
|  |             > | ||||||
|  |               <ha-icon-button | ||||||
|  |                 icon="hass:skip-previous" | ||||||
|  |                 on-click="handlePrevious" | ||||||
|  |                 hidden$="[[!playerObj.supportsPreviousTrack]]" | ||||||
|  |               ></ha-icon-button> | ||||||
|  |               <ha-icon-button | ||||||
|  |                 icon="[[computePlaybackControlIcon(playerObj)]]" | ||||||
|  |                 on-click="handlePlaybackControl" | ||||||
|  |                 hidden$="[[!computePlaybackControlIcon(playerObj)]]" | ||||||
|  |                 highlight="" | ||||||
|  |               ></ha-icon-button> | ||||||
|  |               <ha-icon-button | ||||||
|  |                 icon="hass:skip-next" | ||||||
|  |                 on-click="handleNext" | ||||||
|  |                 hidden$="[[!playerObj.supportsNextTrack]]" | ||||||
|  |               ></ha-icon-button> | ||||||
|  |             </template> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <!-- VOLUME --> | ||||||
|  |         <div | ||||||
|  |           class="volume_buttons center horizontal layout" | ||||||
|  |           hidden$="[[computeHideVolumeButtons(playerObj)]]" | ||||||
|  |         > | ||||||
|  |           <ha-icon-button | ||||||
|  |             on-click="handleVolumeTap" | ||||||
|  |             icon="hass:volume-off" | ||||||
|  |           ></ha-icon-button> | ||||||
|  |           <ha-icon-button | ||||||
|  |             id="volumeDown" | ||||||
|  |             disabled$="[[playerObj.isMuted]]" | ||||||
|  |             on-mousedown="handleVolumeDown" | ||||||
|  |             on-touchstart="handleVolumeDown" | ||||||
|  |             on-touchend="handleVolumeTouchEnd" | ||||||
|  |             icon="hass:volume-medium" | ||||||
|  |           ></ha-icon-button> | ||||||
|  |           <ha-icon-button | ||||||
|  |             id="volumeUp" | ||||||
|  |             disabled$="[[playerObj.isMuted]]" | ||||||
|  |             on-mousedown="handleVolumeUp" | ||||||
|  |             on-touchstart="handleVolumeUp" | ||||||
|  |             on-touchend="handleVolumeTouchEnd" | ||||||
|  |             icon="hass:volume-high" | ||||||
|  |           ></ha-icon-button> | ||||||
|  |         </div> | ||||||
|  |         <div | ||||||
|  |           class="volume center horizontal layout" | ||||||
|  |           hidden$="[[!playerObj.supportsVolumeSet]]" | ||||||
|  |         > | ||||||
|  |           <ha-icon-button | ||||||
|  |             on-click="handleVolumeTap" | ||||||
|  |             hidden$="[[playerObj.supportsVolumeButtons]]" | ||||||
|  |             icon="[[computeMuteVolumeIcon(playerObj)]]" | ||||||
|  |           ></ha-icon-button> | ||||||
|  |           <ha-paper-slider | ||||||
|  |             disabled$="[[playerObj.isMuted]]" | ||||||
|  |             min="0" | ||||||
|  |             max="100" | ||||||
|  |             value="[[playerObj.volumeSliderValue]]" | ||||||
|  |             on-change="volumeSliderChanged" | ||||||
|  |             class="flex" | ||||||
|  |             ignore-bar-touch="" | ||||||
|  |             dir="{{rtl}}" | ||||||
|  |           > | ||||||
|  |           </ha-paper-slider> | ||||||
|  |         </div> | ||||||
|  |         <!-- SOURCE PICKER --> | ||||||
|  |         <div | ||||||
|  |           class="controls layout horizontal justified" | ||||||
|  |           hidden$="[[computeHideSelectSource(playerObj)]]" | ||||||
|  |         > | ||||||
|  |           <ha-icon class="source-input" icon="hass:login-variant"></ha-icon> | ||||||
|  |           <ha-paper-dropdown-menu | ||||||
|  |             class="flex source-input" | ||||||
|  |             dynamic-align="" | ||||||
|  |             label-float="" | ||||||
|  |             label="[[localize('ui.card.media_player.source')]]" | ||||||
|  |           > | ||||||
|  |             <paper-listbox | ||||||
|  |               slot="dropdown-content" | ||||||
|  |               attr-for-selected="item-name" | ||||||
|  |               selected="[[playerObj.source]]" | ||||||
|  |               on-selected-changed="handleSourceChanged" | ||||||
|  |             > | ||||||
|  |               <template is="dom-repeat" items="[[playerObj.sourceList]]"> | ||||||
|  |                 <paper-item item-name$="[[item]]">[[item]]</paper-item> | ||||||
|  |               </template> | ||||||
|  |             </paper-listbox> | ||||||
|  |           </ha-paper-dropdown-menu> | ||||||
|  |         </div> | ||||||
|  |         <!-- SOUND MODE PICKER --> | ||||||
|  |         <template is="dom-if" if="[[!computeHideSelectSoundMode(playerObj)]]"> | ||||||
|  |           <div class="controls layout horizontal justified"> | ||||||
|  |             <ha-icon class="source-input" icon="hass:music-note"></ha-icon> | ||||||
|  |             <ha-paper-dropdown-menu | ||||||
|  |               class="flex source-input" | ||||||
|  |               dynamic-align | ||||||
|  |               label-float | ||||||
|  |               label="[[localize('ui.card.media_player.sound_mode')]]" | ||||||
|  |             > | ||||||
|  |               <paper-listbox | ||||||
|  |                 slot="dropdown-content" | ||||||
|  |                 attr-for-selected="item-name" | ||||||
|  |                 selected="[[playerObj.soundMode]]" | ||||||
|  |                 on-selected-changed="handleSoundModeChanged" | ||||||
|  |               > | ||||||
|  |                 <template is="dom-repeat" items="[[playerObj.soundModeList]]"> | ||||||
|  |                   <paper-item item-name$="[[item]]">[[item]]</paper-item> | ||||||
|  |                 </template> | ||||||
|  |               </paper-listbox> | ||||||
|  |             </ha-paper-dropdown-menu> | ||||||
|  |           </div> | ||||||
|  |         </template> | ||||||
|  |         <!-- TTS --> | ||||||
|  |         <div | ||||||
|  |           hidden$="[[computeHideTTS(ttsLoaded, playerObj)]]" | ||||||
|  |           class="layout horizontal end" | ||||||
|  |         > | ||||||
|  |           <paper-input | ||||||
|  |             id="ttsInput" | ||||||
|  |             label="[[localize('ui.card.media_player.text_to_speak')]]" | ||||||
|  |             class="flex" | ||||||
|  |             value="{{ttsMessage}}" | ||||||
|  |             on-keydown="ttsCheckForEnter" | ||||||
|  |           ></paper-input> | ||||||
|  |           <ha-icon-button icon="hass:send" on-click="sendTTS"></ha-icon-button> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static get properties() { | ||||||
|  |     return { | ||||||
|  |       hass: Object, | ||||||
|  |       stateObj: Object, | ||||||
|  |       playerObj: { | ||||||
|  |         type: Object, | ||||||
|  |         computed: "computePlayerObj(hass, stateObj)", | ||||||
|  |         observer: "playerObjChanged", | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       ttsLoaded: { | ||||||
|  |         type: Boolean, | ||||||
|  |         computed: "computeTTSLoaded(hass)", | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       ttsMessage: { | ||||||
|  |         type: String, | ||||||
|  |         value: "", | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       rtl: { | ||||||
|  |         type: String, | ||||||
|  |         computed: "_computeRTLDirection(hass)", | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   computePlayerObj(hass, stateObj) { | ||||||
|  |     return new HassMediaPlayerEntity(hass, stateObj); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   playerObjChanged(newVal, oldVal) { | ||||||
|  |     if (oldVal) { | ||||||
|  |       setTimeout(() => { | ||||||
|  |         this.fire("iron-resize"); | ||||||
|  |       }, 500); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   computeClassNames(stateObj) { | ||||||
|  |     return attributeClassNames(stateObj, ["volume_level"]); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   computeMuteVolumeIcon(playerObj) { | ||||||
|  |     return playerObj.isMuted ? "hass:volume-off" : "hass:volume-high"; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   computeHideVolumeButtons(playerObj) { | ||||||
|  |     return !playerObj.supportsVolumeButtons || playerObj.isOff; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   computeShowPlaybackControls(playerObj) { | ||||||
|  |     return !playerObj.isOff && playerObj.hasMediaControl; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   computePlaybackControlIcon(playerObj) { | ||||||
|  |     if (playerObj.isPlaying) { | ||||||
|  |       return playerObj.supportsPause ? "hass:pause" : "hass:stop"; | ||||||
|  |     } | ||||||
|  |     if (playerObj.hasMediaControl || playerObj.isOff || playerObj.isIdle) { | ||||||
|  |       if ( | ||||||
|  |         playerObj.hasMediaControl && | ||||||
|  |         playerObj.supportsPause && | ||||||
|  |         !playerObj.isPaused | ||||||
|  |       ) { | ||||||
|  |         return "hass:play-pause"; | ||||||
|  |       } | ||||||
|  |       return playerObj.supportsPlay ? "hass:play" : null; | ||||||
|  |     } | ||||||
|  |     return ""; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   computeHidePowerButton(playerObj) { | ||||||
|  |     return playerObj.isOff | ||||||
|  |       ? !playerObj.supportsTurnOn | ||||||
|  |       : !playerObj.supportsTurnOff; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   computeHideSelectSource(playerObj) { | ||||||
|  |     return ( | ||||||
|  |       playerObj.isOff || | ||||||
|  |       !playerObj.supportsSelectSource || | ||||||
|  |       !playerObj.sourceList | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   computeHideSelectSoundMode(playerObj) { | ||||||
|  |     return ( | ||||||
|  |       playerObj.isOff || | ||||||
|  |       !playerObj.supportsSelectSoundMode || | ||||||
|  |       !playerObj.soundModeList | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   computeHideTTS(ttsLoaded, playerObj) { | ||||||
|  |     return !ttsLoaded || !playerObj.supportsPlayMedia; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   computeTTSLoaded(hass) { | ||||||
|  |     return isComponentLoaded(hass, "tts"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   handleTogglePower() { | ||||||
|  |     this.playerObj.togglePower(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   handlePrevious() { | ||||||
|  |     this.playerObj.previousTrack(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   handlePlaybackControl() { | ||||||
|  |     this.playerObj.mediaPlayPause(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   handleNext() { | ||||||
|  |     this.playerObj.nextTrack(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   handleSourceChanged(ev) { | ||||||
|  |     if (!this.playerObj) return; | ||||||
|  |  | ||||||
|  |     const oldVal = this.playerObj.source; | ||||||
|  |     const newVal = ev.detail.value; | ||||||
|  |  | ||||||
|  |     if (!newVal || oldVal === newVal) return; | ||||||
|  |  | ||||||
|  |     this.playerObj.selectSource(newVal); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   handleSoundModeChanged(ev) { | ||||||
|  |     if (!this.playerObj) return; | ||||||
|  |  | ||||||
|  |     const oldVal = this.playerObj.soundMode; | ||||||
|  |     const newVal = ev.detail.value; | ||||||
|  |  | ||||||
|  |     if (!newVal || oldVal === newVal) return; | ||||||
|  |  | ||||||
|  |     this.playerObj.selectSoundMode(newVal); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   handleVolumeTap() { | ||||||
|  |     if (!this.playerObj.supportsVolumeMute) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this.playerObj.volumeMute(!this.playerObj.isMuted); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   handleVolumeTouchEnd(ev) { | ||||||
|  |     /* when touch ends, we must prevent this from | ||||||
|  |      * becoming a mousedown, up, click by emulation */ | ||||||
|  |     ev.preventDefault(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   handleVolumeUp() { | ||||||
|  |     const obj = this.$.volumeUp; | ||||||
|  |     this.handleVolumeWorker("volume_up", obj, true); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   handleVolumeDown() { | ||||||
|  |     const obj = this.$.volumeDown; | ||||||
|  |     this.handleVolumeWorker("volume_down", obj, true); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   handleVolumeWorker(service, obj, force) { | ||||||
|  |     if (force || (obj !== undefined && obj.pointerDown)) { | ||||||
|  |       this.playerObj.callService(service); | ||||||
|  |       setTimeout(() => this.handleVolumeWorker(service, obj, false), 500); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   volumeSliderChanged(ev) { | ||||||
|  |     const volPercentage = parseFloat(ev.target.value); | ||||||
|  |     const volume = volPercentage > 0 ? volPercentage / 100 : 0; | ||||||
|  |     this.playerObj.setVolume(volume); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ttsCheckForEnter(ev) { | ||||||
|  |     if (ev.keyCode === 13) this.sendTTS(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   sendTTS() { | ||||||
|  |     const services = this.hass.services.tts; | ||||||
|  |     const serviceKeys = Object.keys(services).sort(); | ||||||
|  |     let service; | ||||||
|  |     let i; | ||||||
|  |  | ||||||
|  |     for (i = 0; i < serviceKeys.length; i++) { | ||||||
|  |       if (serviceKeys[i].indexOf("_say") !== -1) { | ||||||
|  |         service = serviceKeys[i]; | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!service) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.hass.callService("tts", service, { | ||||||
|  |       entity_id: this.stateObj.entity_id, | ||||||
|  |       message: this.ttsMessage, | ||||||
|  |     }); | ||||||
|  |     this.ttsMessage = ""; | ||||||
|  |     this.$.ttsInput.focus(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   _computeRTLDirection(hass) { | ||||||
|  |     return computeRTLDirection(hass); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | customElements.define("more-info-media_player", MoreInfoMediaPlayer); | ||||||
| @@ -1,431 +0,0 @@ | |||||||
| import "@material/mwc-button/mwc-button"; |  | ||||||
| import "@polymer/paper-input/paper-input"; |  | ||||||
| import "@polymer/paper-item/paper-item"; |  | ||||||
| import "@polymer/paper-listbox/paper-listbox"; |  | ||||||
| import { |  | ||||||
|   css, |  | ||||||
|   CSSResult, |  | ||||||
|   customElement, |  | ||||||
|   html, |  | ||||||
|   LitElement, |  | ||||||
|   property, |  | ||||||
|   query, |  | ||||||
|   TemplateResult, |  | ||||||
| } from "lit-element"; |  | ||||||
| import { isComponentLoaded } from "../../../common/config/is_component_loaded"; |  | ||||||
| import { supportsFeature } from "../../../common/entity/supports-feature"; |  | ||||||
| import { computeRTLDirection } from "../../../common/util/compute_rtl"; |  | ||||||
| import "../../../components/ha-icon"; |  | ||||||
| import "../../../components/ha-icon-button"; |  | ||||||
| import "../../../components/ha-paper-dropdown-menu"; |  | ||||||
| import "../../../components/ha-slider"; |  | ||||||
| import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog"; |  | ||||||
| import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity"; |  | ||||||
| import { |  | ||||||
|   ControlButton, |  | ||||||
|   MediaPickedEvent, |  | ||||||
|   SUPPORTS_PLAY, |  | ||||||
|   SUPPORT_BROWSE_MEDIA, |  | ||||||
|   SUPPORT_NEXT_TRACK, |  | ||||||
|   SUPPORT_PAUSE, |  | ||||||
|   SUPPORT_PLAY_MEDIA, |  | ||||||
|   SUPPORT_PREVIOUS_TRACK, |  | ||||||
|   SUPPORT_SELECT_SOUND_MODE, |  | ||||||
|   SUPPORT_SELECT_SOURCE, |  | ||||||
|   SUPPORT_STOP, |  | ||||||
|   SUPPORT_TURN_OFF, |  | ||||||
|   SUPPORT_TURN_ON, |  | ||||||
|   SUPPORT_VOLUME_BUTTONS, |  | ||||||
|   SUPPORT_VOLUME_MUTE, |  | ||||||
|   SUPPORT_VOLUME_SET, |  | ||||||
| } from "../../../data/media-player"; |  | ||||||
| import { HomeAssistant, MediaEntity } from "../../../types"; |  | ||||||
|  |  | ||||||
| @customElement("more-info-media_player") |  | ||||||
| class MoreInfoMediaPlayer extends LitElement { |  | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   @property({ attribute: false }) public stateObj?: MediaEntity; |  | ||||||
|  |  | ||||||
|   @query("#ttsInput") private _ttsInput?: HTMLInputElement; |  | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |  | ||||||
|     if (!this.stateObj) { |  | ||||||
|       return html``; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const controls = this._getControls(); |  | ||||||
|     const stateObj = this.stateObj; |  | ||||||
|  |  | ||||||
|     return html` |  | ||||||
|       ${!controls |  | ||||||
|         ? "" |  | ||||||
|         : html` |  | ||||||
|             <div class="controls"> |  | ||||||
|               <div class="basic-controls"> |  | ||||||
|                 ${controls!.map( |  | ||||||
|                   (control) => html` |  | ||||||
|                     <ha-icon-button |  | ||||||
|                       action=${control.action} |  | ||||||
|                       .icon=${control.icon} |  | ||||||
|                       @click=${this._handleClick} |  | ||||||
|                     ></ha-icon-button> |  | ||||||
|                   ` |  | ||||||
|                 )} |  | ||||||
|               </div> |  | ||||||
|               ${supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA) |  | ||||||
|                 ? html` |  | ||||||
|                     <ha-icon-button |  | ||||||
|                       icon="hass:folder-multiple" |  | ||||||
|                       .title=${this.hass.localize( |  | ||||||
|                         "ui.card.media_player.browse_media" |  | ||||||
|                       )} |  | ||||||
|                       @click=${this._showBrowseMedia} |  | ||||||
|                     > |  | ||||||
|                     </ha-icon-button> |  | ||||||
|                   ` |  | ||||||
|                 : ""} |  | ||||||
|             </div> |  | ||||||
|           `} |  | ||||||
|       ${(supportsFeature(stateObj, SUPPORT_VOLUME_SET) || |  | ||||||
|         supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS)) && |  | ||||||
|       ![UNAVAILABLE, UNKNOWN, "off"].includes(stateObj.state) |  | ||||||
|         ? html` |  | ||||||
|             <div class="volume"> |  | ||||||
|               ${supportsFeature(stateObj, SUPPORT_VOLUME_MUTE) |  | ||||||
|                 ? html` |  | ||||||
|                     <ha-icon-button |  | ||||||
|                       .icon=${stateObj.attributes.is_volume_muted |  | ||||||
|                         ? "hass:volume-off" |  | ||||||
|                         : "hass:volume-high"} |  | ||||||
|                       @click=${this._toggleMute} |  | ||||||
|                     ></ha-icon-button> |  | ||||||
|                   ` |  | ||||||
|                 : ""} |  | ||||||
|               ${supportsFeature(stateObj, SUPPORT_VOLUME_SET) |  | ||||||
|                 ? html` |  | ||||||
|                     <ha-slider |  | ||||||
|                       id="input" |  | ||||||
|                       pin |  | ||||||
|                       ignore-bar-touch |  | ||||||
|                       .dir=${computeRTLDirection(this.hass!)} |  | ||||||
|                       .value=${Number(stateObj.attributes.volume_level) * 100} |  | ||||||
|                       @change=${this._selectedValueChanged} |  | ||||||
|                     ></ha-slider> |  | ||||||
|                   ` |  | ||||||
|                 : supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS) |  | ||||||
|                 ? html` |  | ||||||
|                     <ha-icon-button |  | ||||||
|                       action="volume_down" |  | ||||||
|                       icon="hass:volume-minus" |  | ||||||
|                       @click=${this._handleClick} |  | ||||||
|                     ></ha-icon-button> |  | ||||||
|                     <ha-icon-button |  | ||||||
|                       action="volume_up" |  | ||||||
|                       icon="hass:volume-plus" |  | ||||||
|                       @click=${this._handleClick} |  | ||||||
|                     ></ha-icon-button> |  | ||||||
|                   ` |  | ||||||
|                 : ""} |  | ||||||
|             </div> |  | ||||||
|           ` |  | ||||||
|         : ""} |  | ||||||
|       ${![UNAVAILABLE, UNKNOWN, "off"].includes(stateObj.state) && |  | ||||||
|       supportsFeature(stateObj, SUPPORT_SELECT_SOURCE) && |  | ||||||
|       stateObj.attributes.source_list?.length |  | ||||||
|         ? html` |  | ||||||
|             <div class="source-input"> |  | ||||||
|               <ha-icon class="source-input" icon="hass:login-variant"></ha-icon> |  | ||||||
|               <ha-paper-dropdown-menu |  | ||||||
|                 .label=${this.hass.localize("ui.card.media_player.source")} |  | ||||||
|               > |  | ||||||
|                 <paper-listbox |  | ||||||
|                   slot="dropdown-content" |  | ||||||
|                   attr-for-selected="item-name" |  | ||||||
|                   .selected=${stateObj.attributes.source!} |  | ||||||
|                   @iron-select=${this._handleSourceChanged} |  | ||||||
|                 > |  | ||||||
|                   ${stateObj.attributes.source_list!.map( |  | ||||||
|                     (source) => |  | ||||||
|                       html` |  | ||||||
|                         <paper-item .itemName=${source}>${source}</paper-item> |  | ||||||
|                       ` |  | ||||||
|                   )} |  | ||||||
|                 </paper-listbox> |  | ||||||
|               </ha-paper-dropdown-menu> |  | ||||||
|             </div> |  | ||||||
|           ` |  | ||||||
|         : ""} |  | ||||||
|       ${supportsFeature(stateObj, SUPPORT_SELECT_SOUND_MODE) && |  | ||||||
|       stateObj.attributes.sound_mode_list?.length |  | ||||||
|         ? html` |  | ||||||
|             <div class="sound-input"> |  | ||||||
|               <ha-icon icon="hass:music-note"></ha-icon> |  | ||||||
|               <ha-paper-dropdown-menu |  | ||||||
|                 dynamic-align |  | ||||||
|                 label-float |  | ||||||
|                 .label=${this.hass.localize("ui.card.media_player.sound_mode")} |  | ||||||
|               > |  | ||||||
|                 <paper-listbox |  | ||||||
|                   slot="dropdown-content" |  | ||||||
|                   attr-for-selected="item-name" |  | ||||||
|                   .selected=${stateObj.attributes.sound_mode!} |  | ||||||
|                   @iron-select=${this._handleSoundModeChanged} |  | ||||||
|                 > |  | ||||||
|                   ${stateObj.attributes.sound_mode_list.map( |  | ||||||
|                     (mode) => html` |  | ||||||
|                       <paper-item .itemName=${mode}>${mode}</paper-item> |  | ||||||
|                     ` |  | ||||||
|                   )} |  | ||||||
|                 </paper-listbox> |  | ||||||
|               </ha-paper-dropdown-menu> |  | ||||||
|             </div> |  | ||||||
|           ` |  | ||||||
|         : ""} |  | ||||||
|       ${isComponentLoaded(this.hass, "tts") && |  | ||||||
|       supportsFeature(stateObj, SUPPORT_PLAY_MEDIA) |  | ||||||
|         ? html` |  | ||||||
|             <div class="tts"> |  | ||||||
|               <paper-input |  | ||||||
|                 id="ttsInput" |  | ||||||
|                 .label=${this.hass.localize( |  | ||||||
|                   "ui.card.media_player.text_to_speak" |  | ||||||
|                 )} |  | ||||||
|                 @keydown=${this._ttsCheckForEnter} |  | ||||||
|               ></paper-input> |  | ||||||
|               <ha-icon-button icon="hass:send" @click=${ |  | ||||||
|                 this._sendTTS |  | ||||||
|               }></ha-icon-button> |  | ||||||
|             </div> |  | ||||||
|           </div> |  | ||||||
|           ` |  | ||||||
|         : ""} |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResult { |  | ||||||
|     return css` |  | ||||||
|       ha-icon-button[action="turn_off"], |  | ||||||
|       ha-icon-button[action="turn_on"], |  | ||||||
|       ha-slider, |  | ||||||
|       #ttsInput { |  | ||||||
|         flex-grow: 1; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .controls { |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .basic-controls { |  | ||||||
|         flex-grow: 1; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .volume, |  | ||||||
|       .source-input, |  | ||||||
|       .sound-input, |  | ||||||
|       .tts { |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         justify-content: space-between; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .source-input ha-icon, |  | ||||||
|       .sound-input ha-icon { |  | ||||||
|         padding: 7px; |  | ||||||
|         margin-top: 24px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .source-input ha-paper-dropdown-menu, |  | ||||||
|       .sound-input ha-paper-dropdown-menu { |  | ||||||
|         margin-left: 10px; |  | ||||||
|         flex-grow: 1; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       paper-item { |  | ||||||
|         cursor: pointer; |  | ||||||
|       } |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _getControls(): ControlButton[] | undefined { |  | ||||||
|     const stateObj = this.stateObj; |  | ||||||
|  |  | ||||||
|     if (!stateObj) { |  | ||||||
|       return undefined; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const state = stateObj.state; |  | ||||||
|  |  | ||||||
|     if (UNAVAILABLE_STATES.includes(state)) { |  | ||||||
|       return undefined; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (state === "off") { |  | ||||||
|       return supportsFeature(stateObj, SUPPORT_TURN_ON) |  | ||||||
|         ? [ |  | ||||||
|             { |  | ||||||
|               icon: "hass:power", |  | ||||||
|               action: "turn_on", |  | ||||||
|             }, |  | ||||||
|           ] |  | ||||||
|         : undefined; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (state === "idle") { |  | ||||||
|       return supportsFeature(stateObj, SUPPORTS_PLAY) |  | ||||||
|         ? [ |  | ||||||
|             { |  | ||||||
|               icon: "hass:play", |  | ||||||
|               action: "media_play", |  | ||||||
|             }, |  | ||||||
|           ] |  | ||||||
|         : undefined; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const buttons: ControlButton[] = []; |  | ||||||
|  |  | ||||||
|     if (supportsFeature(stateObj, SUPPORT_TURN_OFF)) { |  | ||||||
|       buttons.push({ |  | ||||||
|         icon: "hass:power", |  | ||||||
|         action: "turn_off", |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK)) { |  | ||||||
|       buttons.push({ |  | ||||||
|         icon: "hass:skip-previous", |  | ||||||
|         action: "media_previous_track", |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if ( |  | ||||||
|       (state === "playing" && |  | ||||||
|         (supportsFeature(stateObj, SUPPORT_PAUSE) || |  | ||||||
|           supportsFeature(stateObj, SUPPORT_STOP))) || |  | ||||||
|       (state === "paused" && supportsFeature(stateObj, SUPPORTS_PLAY)) |  | ||||||
|     ) { |  | ||||||
|       buttons.push({ |  | ||||||
|         icon: |  | ||||||
|           state !== "playing" |  | ||||||
|             ? "hass:play" |  | ||||||
|             : supportsFeature(stateObj, SUPPORT_PAUSE) |  | ||||||
|             ? "hass:pause" |  | ||||||
|             : "hass:stop", |  | ||||||
|         action: "media_play_pause", |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (supportsFeature(stateObj, SUPPORT_NEXT_TRACK)) { |  | ||||||
|       buttons.push({ |  | ||||||
|         icon: "hass:skip-next", |  | ||||||
|         action: "media_next_track", |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return buttons.length > 0 ? buttons : undefined; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _handleClick(e: MouseEvent): void { |  | ||||||
|     this.hass!.callService( |  | ||||||
|       "media_player", |  | ||||||
|       (e.currentTarget! as HTMLElement).getAttribute("action")!, |  | ||||||
|       { |  | ||||||
|         entity_id: this.stateObj!.entity_id, |  | ||||||
|       } |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _toggleMute() { |  | ||||||
|     this.hass!.callService("media_player", "volume_mute", { |  | ||||||
|       entity_id: this.stateObj!.entity_id, |  | ||||||
|       is_volume_muted: !this.stateObj!.attributes.is_volume_muted, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _selectedValueChanged(e: Event): void { |  | ||||||
|     this.hass!.callService("media_player", "volume_set", { |  | ||||||
|       entity_id: this.stateObj!.entity_id, |  | ||||||
|       volume_level: |  | ||||||
|         Number((e.currentTarget! as HTMLElement).getAttribute("value")!) / 100, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _handleSourceChanged(e: CustomEvent) { |  | ||||||
|     const newVal = e.detail.item.itemName; |  | ||||||
|  |  | ||||||
|     if (!newVal || this.stateObj!.attributes.source === newVal) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.hass.callService("media_player", "select_source", { |  | ||||||
|       entity_id: this.stateObj!.entity_id, |  | ||||||
|       source: newVal, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _handleSoundModeChanged(e: CustomEvent) { |  | ||||||
|     const newVal = e.detail.item.itemName; |  | ||||||
|  |  | ||||||
|     if (!newVal || this.stateObj?.attributes.sound_mode === newVal) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.hass.callService("media_player", "select_sound_mode", { |  | ||||||
|       entity_id: this.stateObj!.entity_id, |  | ||||||
|       sound_mode: newVal, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _ttsCheckForEnter(e: KeyboardEvent) { |  | ||||||
|     if (e.keyCode === 13) this._sendTTS(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _sendTTS() { |  | ||||||
|     const ttsInput = this._ttsInput; |  | ||||||
|     if (!ttsInput) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const services = this.hass.services.tts; |  | ||||||
|     const serviceKeys = Object.keys(services).sort(); |  | ||||||
|  |  | ||||||
|     const service = serviceKeys.find((key) => key.indexOf("_say") !== -1); |  | ||||||
|  |  | ||||||
|     if (!service) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.hass.callService("tts", service, { |  | ||||||
|       entity_id: this.stateObj!.entity_id, |  | ||||||
|       message: ttsInput.value, |  | ||||||
|     }); |  | ||||||
|     ttsInput.value = ""; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _showBrowseMedia(): void { |  | ||||||
|     showMediaBrowserDialog(this, { |  | ||||||
|       action: "play", |  | ||||||
|       entityId: this.stateObj!.entity_id, |  | ||||||
|       mediaPickedCallback: (pickedMedia: MediaPickedEvent) => |  | ||||||
|         this._playMedia( |  | ||||||
|           pickedMedia.item.media_content_id, |  | ||||||
|           pickedMedia.item.media_content_type |  | ||||||
|         ), |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _playMedia(media_content_id: string, media_content_type: string) { |  | ||||||
|     this.hass!.callService("media_player", "play_media", { |  | ||||||
|       entity_id: this.stateObj!.entity_id, |  | ||||||
|       media_content_id, |  | ||||||
|       media_content_type, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "more-info-media_player": MoreInfoMediaPlayer; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user