Media More Info: Convert to Lit Element (#6619)

* lit element

* Remove Properties

* review comments

* This should be somewhat better.
This commit is contained in:
Zack Arnett 2020-08-17 11:24:19 -05:00 committed by GitHub
parent 21644ec889
commit 39f24c41ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 393 additions and 426 deletions

View File

@ -21,6 +21,11 @@ export interface MediaPlayerThumbnail {
content: string;
}
export interface ControlButton {
icon: string;
action: string;
}
export const getCurrentProgress = (stateObj: HassEntity): number => {
let progress = stateObj.attributes.media_position;

View File

@ -1,421 +0,0 @@
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);

View File

@ -0,0 +1,381 @@
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-input/paper-input";
import {
css,
CSSResult,
html,
LitElement,
property,
TemplateResult,
customElement,
query,
} from "lit-element";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { HomeAssistant, MediaEntity } from "../../../types";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { UNAVAILABLE_STATES, UNAVAILABLE, UNKNOWN } from "../../../data/entity";
import {
SUPPORT_TURN_ON,
SUPPORT_TURN_OFF,
SUPPORTS_PLAY,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_PAUSE,
SUPPORT_STOP,
SUPPORT_NEXT_TRACK,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_BUTTONS,
SUPPORT_SELECT_SOURCE,
SUPPORT_SELECT_SOUND_MODE,
SUPPORT_PLAY_MEDIA,
ControlButton,
} from "../../../data/media-player";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-paper-dropdown-menu";
import "../../../components/ha-icon-button";
import "../../../components/ha-slider";
import "../../../components/ha-icon";
@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">
${controls!.map(
(control) => html`
<ha-icon-button
action=${control.action}
.icon=${control.icon}
@click=${this._handleClick}
></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>
`
: ""}
${stateObj.state !== "off" &&
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;
}
.volume,
.controls,
.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.value;
if (!newVal || this.stateObj!.attributes.source === newVal) return;
this.hass.callService("media_player", "select_source", {
source: newVal,
});
}
private _handleSoundModeChanged(e: CustomEvent) {
const newVal = e.detail.value;
if (!newVal || this.stateObj?.attributes.sound_mode === newVal) return;
this.hass.callService("media_player", "select_sound_mode", {
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 = "";
}
}
declare global {
interface HTMLElementTagNameMap {
"more-info-media_player": MoreInfoMediaPlayer;
}
}

View File

@ -38,6 +38,7 @@ import {
SUPPORT_STOP,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
ControlButton,
} from "../../../data/media-player";
import type { HomeAssistant, MediaEntity } from "../../../types";
import { contrast } from "../common/color/contrast";
@ -157,11 +158,6 @@ const customGenerator = (colors: Swatch[]) => {
return [foregroundColor, backgroundColor.hex];
};
interface ControlButton {
icon: string;
action: string;
}
@customElement("hui-media-control-card")
export class HuiMediaControlCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {

View File

@ -287,6 +287,12 @@ export type MediaEntity = HassEntityBase & {
media_title: string;
icon?: string;
entity_picture_local?: string;
is_volume_muted?: boolean;
volume_level?: number;
source?: string;
source_list?: string[];
sound_mode?: string;
sound_mode_list?: string[];
};
state:
| "playing"