Skip to content
10 changes: 10 additions & 0 deletions src/data/media-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ import { HomeAssistant } from "../types";
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";

export const SUPPORT_PAUSE = 1;
export const SUPPORT_VOLUME_SET = 4;
export const SUPPORT_VOLUME_MUTE = 8;
export const SUPPORT_PREVIOUS_TRACK = 16;
export const SUPPORT_NEXT_TRACK = 32;
export const SUPPORT_TURN_ON = 128;
export const SUPPORT_TURN_OFF = 256;
export const SUPPORT_PLAY_MEDIA = 512;
export const SUPPORT_VOLUME_BUTTONS = 1024;
export const SUPPORT_SELECT_SOURCE = 2048;
export const SUPPORT_STOP = 4096;
export const SUPPORTS_PLAY = 16384;
export const SUPPORT_SELECT_SOUND_MODE = 65536;
export const OFF_STATES = ["off", "idle"];

export interface MediaPlayerThumbnail {
Expand Down
196 changes: 142 additions & 54 deletions src/panels/lovelace/cards/hui-media-control-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,23 @@ 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 {
OFF_STATES,
SUPPORT_PAUSE,
SUPPORT_TURN_ON,
SUPPORT_TURN_OFF,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_NEXT_TRACK,
SUPPORTS_PLAY,
fetchMediaPlayerThumbnailWithCache,
SUPPORT_STOP,
} 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";
import { UNAVAILABLE } from "../../../data/entity";

@customElement("hui-media-control-card")
export class HuiMediaControlCard extends LitElement implements LovelaceCard {
Expand All @@ -38,6 +49,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {

@property() public hass?: HomeAssistant;
@property() private _config?: MediaControlCardConfig;
@property() private _image?: string;

public getCardSize(): number {
return 3;
Expand Down Expand Up @@ -68,28 +80,39 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
>
`;
}
const image =
stateObj.attributes.entity_picture ||
"../static/images/card_media_player_bg.png";

const picture = this._image || "../static/images/card_media_player_bg.png";

return html`
<ha-card>
<div
class="${classMap({
"no-image": !stateObj.attributes.entity_picture,
ratio: true,
class="ratio ${classMap({
"no-image": !this._image,
})}"
>
<div class="image" style="background-image: url(${image})"></div>
<div class="caption">
<div
class="image"
style="background-image: url(${this.hass.hassUrl(picture)})"
></div>
<div
class="caption ${classMap({
unavailable: stateObj.state === UNAVAILABLE,
})}"
>
${this._config!.name ||
computeStateName(this.hass!.states[this._config!.entity])}
<div class="title">
${this._computeMediaTitle(stateObj)}
${stateObj.attributes.media_title ||
this.hass.localize(`state.media_player.${stateObj.state}`) ||
this.hass.localize(`state.default.${stateObj.state}`) ||
stateObj.state}
</div>
${this._computeSecondaryTitle(stateObj)}
</div>
</div>
${OFF_STATES.includes(stateObj.state)
${OFF_STATES.includes(stateObj.state) ||
!stateObj.attributes.media_duration ||
!stateObj.attributes.media_position
? ""
: html`
<paper-progress
Expand All @@ -99,36 +122,63 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
></paper-progress>
`}
<div class="controls">
<paper-icon-button
icon="hass:power"
.action=${stateObj.state === "off" ? "turn_on" : "turn_off"}
@click=${this._handleClick}
></paper-icon-button>
${(stateObj.state === "off" &&
!supportsFeature(stateObj, SUPPORT_TURN_ON)) ||
(stateObj.state === "on" &&
!supportsFeature(stateObj, SUPPORT_TURN_OFF))
? ""
: html`
<paper-icon-button
icon="hass:power"
.action=${stateObj.state === "off" ? "turn_on" : "turn_off"}
@click=${this._handleClick}
></paper-icon-button>
`}
<div class="playback-controls">
<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>
${!supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK)
? ""
: html`
<paper-icon-button
icon="hass:skip-previous"
.disabled="${OFF_STATES.includes(stateObj.state)}"
.action=${"media_previous_track"}
@click=${this._handleClick}
></paper-icon-button>
`}
${(stateObj.state !== "playing" &&
!supportsFeature(stateObj, SUPPORTS_PLAY)) ||
stateObj.state === UNAVAILABLE ||
(stateObj.state === "playing" &&
!supportsFeature(stateObj, SUPPORT_PAUSE) &&
!supportsFeature(stateObj, SUPPORT_STOP))
? ""
: html`
<paper-icon-button
class="playPauseButton"
.disabled="${OFF_STATES.includes(stateObj.state)}"
.icon=${stateObj.state !== "playing"
? "hass:play"
: supportsFeature(stateObj, SUPPORT_PAUSE)
? "hass:pause"
: "hass:stop"}
.action=${"media_play_pause"}
@click=${this._handleClick}
></paper-icon-button>
`}
${!supportsFeature(stateObj, SUPPORT_NEXT_TRACK)
? ""
: html`
<paper-icon-button
icon="hass:skip-next"
.disabled="${OFF_STATES.includes(stateObj.state)}"
.action=${"media_next_track"}
@click=${this._handleClick}
></paper-icon-button>
`}
</div>
<paper-icon-button
icon="hass:dots-vertical"
@click="${this._handleMoreInfo}"
@click=${this._handleMoreInfo}
></paper-icon-button>
</div>
</ha-card>
Expand Down Expand Up @@ -158,41 +208,66 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}

const oldImage = oldHass
? oldHass.states[this._config.entity].attributes.entity_picture
: undefined;
const newImage = this.hass.states[this._config.entity].attributes
.entity_picture;

if (!newImage || newImage === oldImage) {
this._image = newImage;
return;
}

if (newImage.substr(0, 1) !== "/") {
this._image = newImage;
return;
}

fetchMediaPlayerThumbnailWithCache(this.hass, this._config.entity).then(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@balloob didn't we move the thumbnails away from the ws?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the status on how we should fetch the image? Should it be reverted to using the image URL or is this the best method. This is the method it was using before the conversion.

({ content_type, content }) => {
this._image = `data:${content_type};base64,${content}`;
}
);
}

private _computeMediaTitle(stateObj: HassEntity): string {
let prefix;
let suffix;
private _computeSecondaryTitle(stateObj: HassEntity): string {
let secondaryTitle: string;

switch (stateObj.attributes.media_content_type) {
case "music":
prefix = stateObj.attributes.media_artist;
suffix = stateObj.attributes.media_title;
secondaryTitle = stateObj.attributes.media_artist;
break;
case "playlist":
secondaryTitle = stateObj.attributes.media_playlist;
break;
case "tvshow":
prefix = stateObj.attributes.media_series_title;
suffix = stateObj.attributes.media_title;
secondaryTitle = stateObj.attributes.media_series_title;
if (stateObj.attributes.media_season) {
secondaryTitle += " S" + stateObj.attributes.media_season;

if (stateObj.attributes.media_episode) {
secondaryTitle += "E" + stateObj.attributes.media_episode;
}
}
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 = "";
secondaryTitle = stateObj.attributes.app_name
? stateObj.attributes.app_name
: "";
}

return prefix && suffix ? `${prefix}: ${suffix}` : prefix || suffix || "";
return secondaryTitle;
}

private _handleMoreInfo() {
private _handleMoreInfo(): void {
fireEvent(this, "hass-more-info", {
entityId: this._config!.entity,
});
}

private _handleClick(e: MouseEvent) {
private _handleClick(e: MouseEvent): void {
this.hass!.callService("media_player", (e.currentTarget! as any).action, {
entity_id: this._config!.entity,
});
Expand All @@ -205,6 +280,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
width: 100%;
height: 0;
padding-bottom: 56.25%;
transition: padding-bottom 0.8s;
}

.image {
Expand Down Expand Up @@ -252,6 +328,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
width: 44px;
height: 44px;
}

paper-icon-button {
opacity: var(--dark-primary-opacity);
}
Expand All @@ -270,6 +347,10 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
transition: background-color 0.5s;
}

.playPauseButton[disabled] {
background-color: rgba(0, 0, 0, var(--dark-disabled-opacity));
}

.caption {
position: absolute;
left: 0;
Expand All @@ -283,9 +364,16 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
transition: background-color 0.5s;
}

.caption.unavailable {
background-color: rgba(0, 0, 0, var(--dark-secondary-opacity));
}

.title {
font-size: 1.2em;
margin: 8px 0 4px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}

.progress {
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ export type MediaEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & {
media_duration: number;
media_position: number;
media_title: string;
};
};

Expand Down