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` - - - -
-
-
- -
-
- -
-
- -
- - - -
-
- - - -
- -
- - - - - - -
- - - -
- - -
-
- `; - } - - 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` +
+ + + + ${stateObj.attributes.source_list!.map( + (source) => + html` + ${source} + ` + )} + + +
+ ` + : ""} + ${supportsFeature(stateObj, SUPPORT_SELECT_SOUND_MODE) && + stateObj.attributes.sound_mode_list?.length + ? html` +
+ + + + ${stateObj.attributes.sound_mode_list.map( + (mode) => html` + ${mode} + ` + )} + + +
+ ` + : ""} + ${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"