Extract media controls into method (#5141)

* Extract media controls into method

* address comments

* lint

* Moooorre fixes

* Fix margin

* Update demos

* Very narrow idle players show play button

* Lint

* More stuff

* Marquee on steroids
This commit is contained in:
Paulus Schoutsen 2020-03-12 02:40:03 -07:00 committed by GitHub
parent c7a5f63e33
commit 9b220cc6ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 250 additions and 192 deletions

View File

@ -1,23 +1,36 @@
import { getEntity } from "../../../src/fake_data/entity";
export const createMediaPlayerEntities = () => [
getEntity("media_player", "bedroom", "playing", {
media_content_type: "movie",
media_title: "Epic sax guy 10 hours",
app_name: "YouTube",
friendly_name: "Skip, no pause",
supported_features: 32,
}),
getEntity("media_player", "family_room", "paused", {
friendly_name: "Paused, music",
getEntity("media_player", "music_paused", "paused", {
friendly_name: "Pausing The Music",
media_content_type: "music",
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead",
supported_features: 16417,
supported_features: 64063,
entity_picture: "/images/album_cover.jpg",
media_duration: 300,
media_position: 50,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
}),
getEntity("media_player", "family_room_no_play", "paused", {
friendly_name: "Paused, no play",
getEntity("media_player", "music_playing", "playing", {
friendly_name: "Playing The Music",
media_content_type: "music",
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead",
supported_features: 64063,
entity_picture: "/images/album_cover.jpg",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
}),
getEntity("media_player", "stream_playing", "playing", {
friendly_name: "Playing the Stream",
media_content_type: "movie",
media_title: "Epic sax guy 10 hours",
app_name: "YouTube",
@ -31,25 +44,19 @@ export const createMediaPlayerEntities = () => [
app_name: "Netflix",
supported_features: 1,
}),
getEntity("media_player", "lounge_room", "idle", {
friendly_name: "Screen casting",
media_content_type: "music",
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead",
supported_features: 1,
getEntity("media_player", "sonos_idle", "idle", {
friendly_name: "Sonos Idle",
supported_features: 64063,
}),
getEntity("media_player", "theater", "off", {
friendly_name: "Chromcast Idle",
media_content_type: "movie",
media_title: "Epic sax guy 10 hours",
app_name: "YouTube",
supported_features: 33,
friendly_name: "TV Off",
supported_features: 161,
}),
getEntity("media_player", "android_cast", "playing", {
friendly_name: "Player Off",
friendly_name: "Casting App",
media_title: "Android Screen Casting",
app_name: "Screen Mirroring",
supported_features: 21437,
// supported_features: 21437,
}),
getEntity("media_player", "unavailable", "unavailable", {
friendly_name: "Player Unavailable",

View File

@ -7,24 +7,24 @@ import { createMediaPlayerEntities } from "../data/media_players";
const CONFIGS = [
{
heading: "Skip, no pause",
heading: "Paused music",
config: `
- type: media-control
entity: media_player.bedroom
entity: media_player.music_paused
`,
},
{
heading: "Paused, music",
heading: "Playing music",
config: `
- type: media-control
entity: media_player.family_room
entity: media_player.music_playing
`,
},
{
heading: "Paused, no play",
heading: "Playing stream",
config: `
- type: media-control
entity: media_player.family_room_no_play
entity: media_player.stream_playing
`,
},
{
@ -42,10 +42,10 @@ const CONFIGS = [
`,
},
{
heading: "Chromcast Idle",
heading: "Sonos Idle",
config: `
- type: media-control
entity: media_player.lounge_room
entity: media_player.sonos_idle
`,
},
{

View File

@ -11,17 +11,17 @@ const CONFIGS = [
config: `
- type: entities
entities:
- entity: media_player.bedroom
name: Skip, no pause
- entity: media_player.family_room
name: Paused, music
- entity: media_player.family_room_no_play
- entity: media_player.music_paused
name: Paused music
- entity: media_player.music_playing
name: Playing music
- entity: media_player.stream_playing
name: Paused, no play
- entity: media_player.living_room
name: Pause, No skip, tvshow
- entity: media_player.android_cast
name: Screen casting
- entity: media_player.lounge_room
- entity: media_player.sonos_idle
name: Chromcast Idle
- entity: media_player.theater
name: Player Off

View File

@ -83,9 +83,7 @@ export const domainIcon = (domain: string, state?: string): string => {
return state && state === "unlocked" ? "hass:lock-open" : "hass:lock";
case "media_player":
return state && state !== "off" && state !== "idle"
? "hass:cast-connected"
: "hass:cast";
return state && state === "playing" ? "hass:cast-connected" : "hass:cast";
case "zwave":
switch (state) {

View File

@ -14,7 +14,6 @@ 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 const CONTRAST_RATIO = 3.5;
export interface MediaPlayerThumbnail {
@ -56,9 +55,7 @@ export const computeMediaDescription = (stateObj: HassEntity): string => {
}
break;
default:
secondaryTitle = stateObj.attributes.app_name
? stateObj.attributes.app_name
: "";
secondaryTitle = stateObj.attributes.app_name || "";
}
return secondaryTitle;

View File

@ -33,7 +33,6 @@ import { findEntities } from "../common/find-entites";
import { LovelaceConfig } from "../../../data/lovelace";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity";
import {
OFF_STATES,
SUPPORT_PAUSE,
SUPPORT_TURN_ON,
SUPPORT_PREVIOUS_TRACK,
@ -49,6 +48,8 @@ import {
import "../../../components/ha-card";
import "../../../components/ha-icon";
import "../components/hui-marquee";
// tslint:disable-next-line: no-duplicate-imports
import { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
function getContrastRatio(
rgb1: [number, number, number],
@ -57,6 +58,11 @@ function getContrastRatio(
return Math.round((contrast(rgb1, rgb2) + Number.EPSILON) * 100) / 100;
}
interface ControlButton {
icon: string;
action: string;
}
@customElement("hui-media-control-card")
export class HuiMediaControlCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
@ -118,7 +124,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
return;
}
const stateObj = this.hass.states[this._config.entity] as MediaEntity;
const stateObj = this._stateObj;
if (!stateObj) {
return;
@ -130,7 +136,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
stateObj.state === "playing"
) {
this._progressInterval = window.setInterval(
() => this._updateProgressBar(stateObj),
() => this._updateProgressBar(),
1000
);
}
@ -147,7 +153,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
if (!this.hass || !this._config) {
return html``;
}
const stateObj = this.hass.states[this._config.entity] as MediaEntity;
const stateObj = this._stateObj;
if (!stateObj) {
return html`
@ -162,7 +168,9 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
}
const imageStyle = {
"background-image": `url(${this.hass.hassUrl(this._image)})`,
"background-image": this._image
? `url(${this.hass.hassUrl(this._image)})`
: "none",
width: `${this._cardHeight}px`,
"background-color": this._backgroundColor || "",
};
@ -172,12 +180,19 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
width: `${this._cardHeight}px`,
};
const isOffState = OFF_STATES.includes(stateObj.state);
const state = stateObj.state;
const isOffState = state === "off";
const isUnavailable =
stateObj.state === UNAVAILABLE ||
stateObj.state === UNKNOWN ||
(stateObj.state === "off" && !supportsFeature(stateObj, SUPPORT_TURN_ON));
state === UNAVAILABLE ||
state === UNKNOWN ||
(state === "off" && !supportsFeature(stateObj, SUPPORT_TURN_ON));
const hasNoImage = !this._image;
const controls = this._getControls();
const showControls =
controls && (!this._veryNarrow || isOffState || state === "idle");
const mediaDescription = computeMediaDescription(stateObj);
return html`
<ha-card>
@ -215,7 +230,8 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
"no-image": hasNoImage,
narrow: this._narrow && !this._veryNarrow,
off: isOffState || isUnavailable,
"no-progress": !this._showProgressBar && !this._veryNarrow,
"no-progress": this._veryNarrow || !this._showProgressBar,
"no-controls": !showControls,
})}"
style=${styleMap({ color: this._foregroundColor || "" })}
>
@ -246,98 +262,35 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
: `${this._cardHeight - 40}px`,
})}
>
${isOffState
${!mediaDescription && !stateObj.attributes.media_title
? ""
: html`
<div class="media-info">
<div class="title">
<hui-marquee
.text=${stateObj.attributes.media_title ||
computeMediaDescription(stateObj)}
.active=${this._marqueeActive}
@mouseover=${this._marqueeMouseOver}
@mouseleave=${this._marqueeMouseLeave}
></hui-marquee>
</div>
<hui-marquee
.text=${stateObj.attributes.media_title ||
mediaDescription}
.active=${this._marqueeActive}
@mouseover=${this._marqueeMouseOver}
@mouseleave=${this._marqueeMouseLeave}
></hui-marquee>
${!stateObj.attributes.media_title
? ""
: computeMediaDescription(stateObj)}
: mediaDescription}
</div>
`}
${this._veryNarrow && !isOffState
${!showControls
? ""
: html`
<div class="controls">
<div>
${(stateObj.state === "off" &&
!supportsFeature(stateObj, SUPPORT_TURN_ON)) ||
!isOffState
? ""
: html`
<paper-icon-button
icon="hass:power"
.action=${stateObj.state === "off"
? "turn_on"
: "turn_off"}
@click=${this._handleClick}
></paper-icon-button>
`}
</div>
${isOffState
? ""
: html`
<div class="playback-controls">
${!supportsFeature(
stateObj,
SUPPORT_PREVIOUS_TRACK
)
? ""
: html`
<paper-icon-button
icon="hass:skip-previous"
.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"
.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"
.action=${"media_next_track"}
@click=${this._handleClick}
></paper-icon-button>
`}
</div>
`}
${controls!.map(
(control) => html`
<paper-icon-button
.icon=${control.icon}
action=${control.action}
@click=${this._handleClick}
></paper-icon-button>
`
)}
</div>
`}
</div>
@ -346,7 +299,6 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
: html`
<paper-progress
.max=${stateObj.attributes.media_duration}
class="progress"
style=${styleMap({
"--paper-progress-active-color":
this._foregroundColor || "var(--accent-color)",
@ -354,8 +306,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
? "pointer"
: "initial",
})}
@click=${(e: MouseEvent) =>
this._handleSeek(e, stateObj)}
@click=${this._handleSeek}
></paper-progress>
`}
`}
@ -374,13 +325,20 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass || !changedProps.has("hass")) {
return;
}
const stateObj = this.hass.states[this._config.entity] as MediaEntity;
const stateObj = this._stateObj;
if (!stateObj) {
if (this._progressInterval) {
clearInterval(this._progressInterval);
this._progressInterval = undefined;
}
this._foregroundColor = undefined;
this._backgroundColor = undefined;
return;
}
@ -398,13 +356,15 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
this._updateProgressBar();
if (
!this._progressInterval &&
this._showProgressBar &&
stateObj.state === "playing"
) {
this._progressInterval = window.setInterval(
() => this._updateProgressBar(stateObj),
() => this._updateProgressBar(),
1000
);
} else if (
@ -427,16 +387,86 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
if (this._image !== oldImage) {
this._setColors();
return;
}
}
private _getControls(): ControlButton[] | undefined {
const stateObj = this._stateObj;
if (!stateObj) {
return undefined;
}
const state = stateObj.state;
if (state === UNAVAILABLE || state === UNKNOWN) {
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_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 get _image() {
if (!this.hass || !this._config) {
return undefined;
}
const stateObj = this.hass.states[this._config.entity] as MediaEntity;
const stateObj = this._stateObj;
if (!stateObj) {
return undefined;
@ -449,21 +479,20 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
}
private get _showProgressBar() {
if (!this.hass || !this._config) {
if (!this.hass || !this._config || this._narrow) {
return false;
}
const stateObj = this.hass.states[this._config.entity] as MediaEntity;
const stateObj = this._stateObj;
if (!stateObj) {
return false;
}
return (
!OFF_STATES.includes(stateObj.state) &&
stateObj.attributes.media_duration &&
stateObj.attributes.media_position &&
!this._narrow
(stateObj.state === "playing" || stateObj.state === "paused") &&
"media_duration" in stateObj.attributes &&
"media_position" in stateObj.attributes
);
}
@ -490,7 +519,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
debounce(() => this._measureCard(), 250, false)
);
this._resizeObserver.observe(this);
this._resizeObserver.observe(this.shadowRoot!.querySelector("ha-card")!);
}
private _handleMoreInfo(): void {
@ -500,18 +529,28 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
}
private _handleClick(e: MouseEvent): void {
this.hass!.callService("media_player", (e.currentTarget! as any).action, {
entity_id: this._config!.entity,
});
this.hass!.callService(
"media_player",
(e.currentTarget! as PaperIconButtonElement).getAttribute("action")!,
{
entity_id: this._config!.entity,
}
);
}
private _updateProgressBar(stateObj: MediaEntity): void {
private _updateProgressBar(): void {
if (this._progressBar) {
this._progressBar.value = getCurrentProgress(stateObj);
this._progressBar.value = getCurrentProgress(this._stateObj!);
}
}
private _handleSeek(e: MouseEvent, stateObj: MediaEntity): void {
private get _stateObj(): MediaEntity | undefined {
return this.hass!.states[this._config!.entity] as MediaEntity;
}
private _handleSeek(e: MouseEvent): void {
const stateObj = this._stateObj!;
if (!supportsFeature(stateObj, SUPPORT_SEEK)) {
return;
}
@ -710,9 +749,11 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
height: 44px;
}
.playPauseButton {
width: 56px !important;
height: 56px !important;
paper-icon-button[action="media_play"],
paper-icon-button[action="media_play_pause"],
paper-icon-button[action="turn_on"] {
width: 56px;
height: 56px;
}
.top-info {
@ -742,19 +783,16 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
overflow: hidden;
}
hui-marquee {
font-size: 1.2em;
margin: 0px 0 4px;
}
.title-controls {
padding-top: 16px;
}
.title {
font-size: 1.2em;
margin: 0px 0 4px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.progress {
paper-progress {
width: 100%;
height: var(--paper-progress-height, 4px);
margin-top: 4px;
@ -775,11 +813,6 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
height: 55px;
}
.off.player,
.narrow.player {
padding-bottom: 16px !important;
}
.narrow .controls,
.no-progress .controls {
padding-bottom: 0;
@ -790,12 +823,14 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
height: 40px;
}
.narrow .playPauseButton {
width: 50px !important;
height: 50px !important;
.narrow paper-icon-button[action="media_play"],
.narrow paper-icon-button[action="media_play_pause"],
.narrow paper-icon-button[action="turn_on"] {
width: 50px;
height: 50px;
}
.no-progress.player {
.no-progress.player:not(.no-controls) {
padding-bottom: 0px;
}
`;

View File

@ -8,13 +8,25 @@ import {
CSSResult,
property,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
@customElement("hui-marquee")
class HuiMarquee extends LitElement {
@property() public text?: string;
@property() public active?: boolean;
@property() private _animating = false;
@property({ type: Boolean }) public active?: boolean;
@property({ reflect: true, type: Boolean, attribute: "animating" })
private _animating = false;
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.addEventListener("mouseover", () => this.classList.add("hovering"), {
// Capture because we need to run before a parent sets active on us.
// Hovering will disable the overflow, allowing us to calc if we overflow.
capture: true,
});
this.addEventListener("mouseout", () => this.classList.remove("hovering"));
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
@ -33,12 +45,7 @@ class HuiMarquee extends LitElement {
}
return html`
<div
class="marquee-inner ${classMap({
animating: this._animating,
})}"
@animationiteration=${this._onIteration}
>
<div class="marquee-inner" @animationiteration=${this._onIteration}>
<span>${this.text}</span>
${this._animating
? html`
@ -61,15 +68,29 @@ class HuiMarquee extends LitElement {
display: flex;
position: relative;
align-items: center;
height: 25px;
height: 1em;
}
.marquee-inner {
position: absolute;
left: 0;
right: 0;
text-overflow: ellipsis;
overflow: hidden;
animation: marquee 10s linear infinite paused;
}
.animating {
:host(.hovering) .marquee-inner {
text-overflow: initial;
overflow: initial;
}
:host([animating]) .marquee-inner {
left: initial;
right: initial;
}
:host([animating]) > div {
animation-play-state: running;
}

View File

@ -20,7 +20,6 @@ import { supportsFeature } from "../../../common/entity/supports-feature";
import {
SUPPORTS_PLAY,
SUPPORT_NEXT_TRACK,
OFF_STATES,
SUPPORT_PAUSE,
} from "../../../data/media-player";
import { hasConfigOrEntityChanged } from "../common/has-changed";
@ -68,7 +67,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
.config=${this._config}
.secondaryText=${this._computeMediaTitle(stateObj)}
>
${OFF_STATES.includes(stateObj.state)
${stateObj.state === "off" || stateObj.state === "idle"
? html`
<div class="text-content">
${this.hass!.localize(`state.media_player.${stateObj.state}`) ||

View File

@ -210,6 +210,7 @@ export type MediaEntity = HassEntityBase & {
icon?: string;
entity_picture_local?: string;
};
state: "playing" | "paused" | "idle" | "off" | "unavailable" | "unknown";
};
export type InputSelectEntity = HassEntityBase & {