mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 03:36:44 +00:00
Convert Media Control Card to Typescript (#4761)
* Delete old JS file. Add new TS file. Add Type * Updates * Reviews * Review Updates :) * Updating State Localize * Clean up * Fixing Travis
This commit is contained in:
parent
c7f7ef28bf
commit
2e4c73c087
@ -1,58 +0,0 @@
|
|||||||
import { createErrorCardConfig } from "./hui-error-card";
|
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
|
||||||
|
|
||||||
export default class LegacyWrapperCard extends HTMLElement {
|
|
||||||
constructor(tag, domain) {
|
|
||||||
super();
|
|
||||||
this._tag = tag.toUpperCase();
|
|
||||||
this._domain = domain;
|
|
||||||
this._element = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCardSize() {
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
setConfig(config) {
|
|
||||||
if (!config.entity) {
|
|
||||||
throw new Error("No entity specified");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (computeDomain(config.entity) !== this._domain) {
|
|
||||||
throw new Error(
|
|
||||||
`Specified entity needs to be of domain ${this._domain}.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
set hass(hass) {
|
|
||||||
const entityId = this._config.entity;
|
|
||||||
|
|
||||||
if (entityId in hass.states) {
|
|
||||||
this._ensureElement(this._tag);
|
|
||||||
this.lastChild.hass = hass;
|
|
||||||
this.lastChild.stateObj = hass.states[entityId];
|
|
||||||
this.lastChild.config = this._config;
|
|
||||||
} else {
|
|
||||||
this._ensureElement("HUI-ERROR-CARD");
|
|
||||||
this.lastChild.setConfig(
|
|
||||||
createErrorCardConfig(
|
|
||||||
`No state available for ${entityId}`,
|
|
||||||
this._config
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ensureElement(tag) {
|
|
||||||
if (this.lastChild && this.lastChild.tagName === tag) return;
|
|
||||||
|
|
||||||
if (this.lastChild) {
|
|
||||||
this.removeChild(this.lastChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.appendChild(document.createElement(tag));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
import "../../../cards/ha-media_player-card";
|
|
||||||
|
|
||||||
import LegacyWrapperCard from "./hui-legacy-wrapper-card";
|
|
||||||
|
|
||||||
class HuiMediaControlCard extends LegacyWrapperCard {
|
|
||||||
static async getConfigElement() {
|
|
||||||
await import(
|
|
||||||
/* webpackChunkName: "hui-media-control-card-editor" */ "../editor/config-elements/hui-media-control-card-editor"
|
|
||||||
);
|
|
||||||
return document.createElement("hui-media-control-card-editor");
|
|
||||||
}
|
|
||||||
|
|
||||||
static getStubConfig() {
|
|
||||||
return { entity: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super("ha-media_player-card", "media_player");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("hui-media-control-card", HuiMediaControlCard);
|
|
320
src/panels/lovelace/cards/hui-media-control-card.ts
Normal file
320
src/panels/lovelace/cards/hui-media-control-card.ts
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
import {
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
customElement,
|
||||||
|
property,
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import { classMap } from "lit-html/directives/class-map";
|
||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import "@polymer/paper-icon-button/paper-icon-button";
|
||||||
|
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||||
|
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||||
|
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||||
|
import { OFF_STATES, SUPPORT_PAUSE } from "../../../data/media-player";
|
||||||
|
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||||
|
import { HomeAssistant, MediaEntity } from "../../../types";
|
||||||
|
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { MediaControlCardConfig } from "./types";
|
||||||
|
|
||||||
|
@customElement("hui-media-control-card")
|
||||||
|
export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||||
|
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||||
|
await import(
|
||||||
|
/* webpackChunkName: "hui-media-control-card-editor" */ "../editor/config-elements/hui-media-control-card-editor"
|
||||||
|
);
|
||||||
|
return document.createElement("hui-media-control-card-editor");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getStubConfig(): object {
|
||||||
|
return { entity: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
@property() public hass?: HomeAssistant;
|
||||||
|
@property() private _config?: MediaControlCardConfig;
|
||||||
|
|
||||||
|
public getCardSize(): number {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setConfig(config: MediaControlCardConfig): void {
|
||||||
|
if (!config.entity || config.entity.split(".")[0] !== "media_player") {
|
||||||
|
throw new Error("Specify an entity from within the media_player domain.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this._config = { theme: "default", ...config };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this.hass || !this._config) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
const stateObj = this.hass.states[this._config.entity] as MediaEntity;
|
||||||
|
|
||||||
|
if (!stateObj) {
|
||||||
|
return html`
|
||||||
|
<hui-warning
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.lovelace.warning.entity_not_found",
|
||||||
|
"entity",
|
||||||
|
this._config.entity
|
||||||
|
)}</hui-warning
|
||||||
|
>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
const image =
|
||||||
|
stateObj.attributes.entity_picture ||
|
||||||
|
"../static/images/card_media_player_bg.png";
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-card>
|
||||||
|
<div
|
||||||
|
class="${classMap({
|
||||||
|
"no-image": !stateObj.attributes.entity_picture,
|
||||||
|
ratio: true,
|
||||||
|
})}"
|
||||||
|
>
|
||||||
|
<div class="image" style="background-image: url(${image})"></div>
|
||||||
|
<div class="caption">
|
||||||
|
${this._config!.name ||
|
||||||
|
computeStateName(this.hass!.states[this._config!.entity])}
|
||||||
|
<div class="title">
|
||||||
|
${this._computeMediaTitle(stateObj)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${OFF_STATES.includes(stateObj.state)
|
||||||
|
? ""
|
||||||
|
: html`
|
||||||
|
<paper-progress
|
||||||
|
.max="${stateObj.attributes.media_duration}"
|
||||||
|
.value="${stateObj.attributes.media_position}"
|
||||||
|
class="progress"
|
||||||
|
></paper-progress>
|
||||||
|
`}
|
||||||
|
<div class="controls flex">
|
||||||
|
<div class="left flex">
|
||||||
|
<paper-icon-button
|
||||||
|
icon="hass:power"
|
||||||
|
.action=${stateObj.state === "off" ? "turn_on" : "turn_off"}
|
||||||
|
@click=${this._handleClick}
|
||||||
|
></paper-icon-button>
|
||||||
|
</div>
|
||||||
|
<div class="center flex">
|
||||||
|
<paper-icon-button
|
||||||
|
icon="hass:skip-previous"
|
||||||
|
.action=${"media_previous_track"}
|
||||||
|
@click=${this._handleClick}
|
||||||
|
></paper-icon-button>
|
||||||
|
<paper-icon-button
|
||||||
|
class="playPauseButton"
|
||||||
|
icon=${stateObj.state !== "playing"
|
||||||
|
? "hass:play"
|
||||||
|
: supportsFeature(stateObj, SUPPORT_PAUSE)
|
||||||
|
? "hass:pause"
|
||||||
|
: "hass:stop"}
|
||||||
|
.action=${"media_play_pause"}
|
||||||
|
@click=${this._handleClick}
|
||||||
|
></paper-icon-button>
|
||||||
|
<paper-icon-button
|
||||||
|
icon="hass:skip-next"
|
||||||
|
.action=${"media_next_track"}
|
||||||
|
@click=${this._handleClick}
|
||||||
|
></paper-icon-button>
|
||||||
|
</div>
|
||||||
|
<div class="right flex">
|
||||||
|
<paper-icon-button
|
||||||
|
icon="hass:dots-vertical"
|
||||||
|
@click="${this._handleMoreInfo}"
|
||||||
|
></paper-icon-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
|
return hasConfigOrEntityChanged(this, changedProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues): void {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (!this._config || !this.hass || !changedProps.has("hass")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
|
const oldConfig = changedProps.get("_config") as
|
||||||
|
| MediaControlCardConfig
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!oldHass ||
|
||||||
|
!oldConfig ||
|
||||||
|
oldHass.themes !== this.hass.themes ||
|
||||||
|
oldConfig.theme !== this._config.theme
|
||||||
|
) {
|
||||||
|
applyThemesOnElement(this, this.hass.themes, this._config.theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeMediaTitle(stateObj: HassEntity): string {
|
||||||
|
let prefix;
|
||||||
|
let suffix;
|
||||||
|
|
||||||
|
switch (stateObj.attributes.media_content_type) {
|
||||||
|
case "music":
|
||||||
|
prefix = stateObj.attributes.media_artist;
|
||||||
|
suffix = stateObj.attributes.media_title;
|
||||||
|
break;
|
||||||
|
case "tvshow":
|
||||||
|
prefix = stateObj.attributes.media_series_title;
|
||||||
|
suffix = stateObj.attributes.media_title;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
prefix =
|
||||||
|
stateObj.attributes.media_title ||
|
||||||
|
stateObj.attributes.app_name ||
|
||||||
|
this.hass!.localize(`state.media_player.${stateObj.state}`) ||
|
||||||
|
this.hass!.localize(`state.default.${stateObj.state}`) ||
|
||||||
|
stateObj.state;
|
||||||
|
suffix = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefix && suffix ? `${prefix}: ${suffix}` : prefix || suffix || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleMoreInfo() {
|
||||||
|
fireEvent(this, "hass-more-info", {
|
||||||
|
entityId: this._config!.entity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleClick(e: MouseEvent) {
|
||||||
|
this.hass!.callService("media_player", (e.currentTarget! as any).action, {
|
||||||
|
entity_id: this._config!.entity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult {
|
||||||
|
return css`
|
||||||
|
.ratio {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
padding-bottom: 56.25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
transition: filter 0.2s linear;
|
||||||
|
background-position: center center;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-image {
|
||||||
|
padding-bottom: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-image > .image {
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
background-size: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-image > .caption {
|
||||||
|
background-color: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
align-content: space-evenly;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls > div {
|
||||||
|
width: 33%;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
paper-icon-button {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
opacity: var(--dark-primary-opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
paper-icon-button[disabled] {
|
||||||
|
opacity: var(--dark-disabled-opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playPauseButton {
|
||||||
|
width: 56px !important;
|
||||||
|
height: 56px !important;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 8px;
|
||||||
|
transition: background-color 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, var(--dark-secondary-opacity));
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
transition: background-color 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin: 8px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
width: 100%;
|
||||||
|
height: var(--paper-progress-height, 4px);
|
||||||
|
margin-top: calc(-1 * var(--paper-progress-height, 4px));
|
||||||
|
--paper-progress-active-color: var(--accent-color);
|
||||||
|
--paper-progress-container-color: rgba(200, 200, 200, 0.5);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-media-control-card": HuiMediaControlCard;
|
||||||
|
}
|
||||||
|
}
|
@ -11,9 +11,9 @@ import { EntitiesEditorEvent, EditorTarget } from "../types";
|
|||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
import { LovelaceCardEditor } from "../../types";
|
import { LovelaceCardEditor } from "../../types";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { MediaControlCardConfig } from "../../cards/hui-media-control-card";
|
|
||||||
|
|
||||||
import "../../../../components/entity/ha-entity-picker";
|
import "../../../../components/entity/ha-entity-picker";
|
||||||
|
import { MediaControlCardConfig } from "../../cards/types";
|
||||||
|
|
||||||
const cardConfigStruct = struct({
|
const cardConfigStruct = struct({
|
||||||
type: "string",
|
type: "string",
|
||||||
|
@ -200,6 +200,13 @@ export type CameraEntity = HassEntityBase & {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MediaEntity = HassEntityBase & {
|
||||||
|
attributes: HassEntityAttributeBase & {
|
||||||
|
media_duration: number;
|
||||||
|
media_position: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type InputSelectEntity = HassEntityBase & {
|
export type InputSelectEntity = HassEntityBase & {
|
||||||
attributes: HassEntityAttributeBase & {
|
attributes: HassEntityAttributeBase & {
|
||||||
options: string[];
|
options: string[];
|
||||||
|
Loading…
x
Reference in New Issue
Block a user