diff --git a/src/data/media-player.ts b/src/data/media-player.ts
index 1f716cbee1..71a2939716 100644
--- a/src/data/media-player.ts
+++ b/src/data/media-player.ts
@@ -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;
diff --git a/src/dialogs/more-info/controls/more-info-media_player.js b/src/dialogs/more-info/controls/more-info-media_player.js
deleted file mode 100644
index 2e45403793..0000000000
--- a/src/dialogs/more-info/controls/more-info-media_player.js
+++ /dev/null
@@ -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`
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- [[item]]
-
-
-
-
-
-
-
-
-
-
-
- [[item]]
-
-
-
-
-
-
-
-
- `;
- }
-
- 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);
diff --git a/src/dialogs/more-info/controls/more-info-media_player.ts b/src/dialogs/more-info/controls/more-info-media_player.ts
new file mode 100644
index 0000000000..efa96dec86
--- /dev/null
+++ b/src/dialogs/more-info/controls/more-info-media_player.ts
@@ -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`
+
+ ${controls!.map(
+ (control) => html`
+
+ `
+ )}
+
+ `}
+ ${(supportsFeature(stateObj, SUPPORT_VOLUME_SET) ||
+ supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS)) &&
+ ![UNAVAILABLE, UNKNOWN, "off"].includes(stateObj.state)
+ ? html`
+
+ ${supportsFeature(stateObj, SUPPORT_VOLUME_MUTE)
+ ? html`
+
+ `
+ : ""}
+ ${supportsFeature(stateObj, SUPPORT_VOLUME_SET)
+ ? html`
+
+ `
+ : supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS)
+ ? html`
+
+
+ `
+ : ""}
+
+ `
+ : ""}
+ ${stateObj.state !== "off" &&
+ supportsFeature(stateObj, SUPPORT_SELECT_SOURCE) &&
+ stateObj.attributes.source_list?.length
+ ? html`
+
+ `
+ : ""}
+ ${supportsFeature(stateObj, SUPPORT_SELECT_SOUND_MODE) &&
+ stateObj.attributes.sound_mode_list?.length
+ ? html`
+
+ `
+ : ""}
+ ${isComponentLoaded(this.hass, "tts") &&
+ supportsFeature(stateObj, SUPPORT_PLAY_MEDIA)
+ ? html`
+
+
+ `
+ : ""}
+ `;
+ }
+
+ 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;
+ }
+}
diff --git a/src/panels/lovelace/cards/hui-media-control-card.ts b/src/panels/lovelace/cards/hui-media-control-card.ts
index 51528d8d32..cb6df43058 100644
--- a/src/panels/lovelace/cards/hui-media-control-card.ts
+++ b/src/panels/lovelace/cards/hui-media-control-card.ts
@@ -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 {
diff --git a/src/types.ts b/src/types.ts
index 4b0af2a890..7639f1ea43 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -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"