mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-09 10:59:50 +00:00
Media player playback controls card feature (#26608)
* Setup * Add buttons * Fix * Move to function * Clean * Cleanup * Check client size * Get width from host component * Fix * Spacing * use current target * Ensure state updates update render * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/panels/lovelace/card-features/hui-media-player-playback-card-feature.ts * Resolve code suggestion type errors * Resize observer not needed --------- Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,323 @@
|
|||||||
|
import type { PropertyValues } from "lit";
|
||||||
|
import { html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import {
|
||||||
|
mdiPause,
|
||||||
|
mdiPlay,
|
||||||
|
mdiPlayPause,
|
||||||
|
mdiPower,
|
||||||
|
mdiSkipNext,
|
||||||
|
mdiSkipPrevious,
|
||||||
|
mdiStop,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import type { LovelaceCardFeature } from "../types";
|
||||||
|
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||||
|
import type {
|
||||||
|
LovelaceCardFeatureContext,
|
||||||
|
MediaPlayerPlaybackCardFeatureConfig,
|
||||||
|
} from "./types";
|
||||||
|
import type {
|
||||||
|
ControlButton,
|
||||||
|
MediaPlayerEntity,
|
||||||
|
} from "../../../data/media-player";
|
||||||
|
import { MediaPlayerEntityFeature } from "../../../data/media-player";
|
||||||
|
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||||
|
import { stateActive } from "../../../common/entity/state_active";
|
||||||
|
import { isUnavailableState } from "../../../data/entity";
|
||||||
|
import { hasConfigChanged } from "../common/has-changed";
|
||||||
|
import "../../../components/ha-control-button-group";
|
||||||
|
import "../../../components/ha-control-button";
|
||||||
|
import "../../../components/ha-icon-button";
|
||||||
|
import "../../../components/ha-icon";
|
||||||
|
|
||||||
|
export const supportsMediaPlayerPlaybackCardFeature = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
context: LovelaceCardFeatureContext
|
||||||
|
) => {
|
||||||
|
const stateObj = context.entity_id
|
||||||
|
? hass.states[context.entity_id]
|
||||||
|
: undefined;
|
||||||
|
if (!stateObj) return false;
|
||||||
|
const domain = computeDomain(stateObj.entity_id);
|
||||||
|
return domain === "media_player";
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("hui-media-player-playback-card-feature")
|
||||||
|
class HuiMediaPlayerPlaybackCardFeature
|
||||||
|
extends LitElement
|
||||||
|
implements LovelaceCardFeature
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public color?: string;
|
||||||
|
|
||||||
|
@state() private _config?: MediaPlayerPlaybackCardFeatureConfig;
|
||||||
|
|
||||||
|
@state() private _narrow?: boolean = false;
|
||||||
|
|
||||||
|
private get _stateObj() {
|
||||||
|
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.hass.states[this.context.entity_id] as
|
||||||
|
| MediaPlayerEntity
|
||||||
|
| undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getStubConfig(): MediaPlayerPlaybackCardFeatureConfig {
|
||||||
|
return {
|
||||||
|
type: "media-player-playback",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public setConfig(config: MediaPlayerPlaybackCardFeatureConfig): void {
|
||||||
|
if (!config) {
|
||||||
|
throw new Error("Invalid configuration");
|
||||||
|
}
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public willUpdate(): void {
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
this._measureCard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
|
const entityId = this.context?.entity_id;
|
||||||
|
return (
|
||||||
|
hasConfigChanged(this, changedProps) ||
|
||||||
|
(changedProps.has("hass") &&
|
||||||
|
(!oldHass ||
|
||||||
|
!entityId ||
|
||||||
|
oldHass.states[entityId] !== this.hass!.states[entityId]))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (
|
||||||
|
!this._config ||
|
||||||
|
!this.hass ||
|
||||||
|
!this.context ||
|
||||||
|
!supportsMediaPlayerPlaybackCardFeature(this.hass, this.context) ||
|
||||||
|
!this._stateObj
|
||||||
|
) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttons = this._computeButtons(this._stateObj);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-control-button-group>
|
||||||
|
${supportsFeature(this._stateObj, MediaPlayerEntityFeature.TURN_OFF) &&
|
||||||
|
stateActive(this._stateObj)
|
||||||
|
? html`
|
||||||
|
<ha-control-button
|
||||||
|
.label=${this.hass.localize("ui.card.media_player.turn_off")}
|
||||||
|
@click=${this._togglePower}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiPower}></ha-svg-icon>
|
||||||
|
</ha-control-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${supportsFeature(this._stateObj, MediaPlayerEntityFeature.TURN_ON) &&
|
||||||
|
!stateActive(this._stateObj) &&
|
||||||
|
!isUnavailableState(this._stateObj.state)
|
||||||
|
? html`
|
||||||
|
<ha-control-button
|
||||||
|
.label=${this.hass.localize("ui.card.media_player.turn_on")}
|
||||||
|
@click=${this._togglePower}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiPower}></ha-svg-icon>
|
||||||
|
</ha-control-button>
|
||||||
|
`
|
||||||
|
: buttons.map(
|
||||||
|
(button) => html`
|
||||||
|
<ha-control-button
|
||||||
|
key=${button.action}
|
||||||
|
.label=${this.hass?.localize(
|
||||||
|
`ui.card.media_player.${button.action}`
|
||||||
|
)}
|
||||||
|
@click=${this._action}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${button.icon}></ha-svg-icon>
|
||||||
|
</ha-control-button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-control-button-group>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _measureCard() {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const host = (this.getRootNode() as ShadowRoot).host as
|
||||||
|
| HTMLElement
|
||||||
|
| undefined;
|
||||||
|
const width = host?.clientWidth ?? this.clientWidth ?? 0;
|
||||||
|
this._narrow = width < 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeControlButton(stateObj: MediaPlayerEntity): ControlButton {
|
||||||
|
return stateObj.state === "on"
|
||||||
|
? { icon: mdiPlayPause, action: "media_play_pause" }
|
||||||
|
: stateObj.state !== "playing"
|
||||||
|
? { icon: mdiPlay, action: "media_play" }
|
||||||
|
: supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE)
|
||||||
|
? { icon: mdiPause, action: "media_pause" }
|
||||||
|
: { icon: mdiStop, action: "media_stop" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeButtons(stateObj: MediaPlayerEntity): ControlButton[] {
|
||||||
|
const controlButton = this._computeControlButton(stateObj);
|
||||||
|
const assumedState = stateObj.attributes.assumed_state === true;
|
||||||
|
|
||||||
|
const controls: ControlButton[] = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this._narrow &&
|
||||||
|
(stateObj.state === "playing" || assumedState) &&
|
||||||
|
supportsFeature(stateObj, MediaPlayerEntityFeature.PREVIOUS_TRACK)
|
||||||
|
) {
|
||||||
|
controls.push({ icon: mdiSkipPrevious, action: "media_previous_track" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!assumedState &&
|
||||||
|
((stateObj.state === "playing" &&
|
||||||
|
(supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE) ||
|
||||||
|
supportsFeature(stateObj, MediaPlayerEntityFeature.STOP))) ||
|
||||||
|
((stateObj.state === "paused" || stateObj.state === "idle") &&
|
||||||
|
supportsFeature(stateObj, MediaPlayerEntityFeature.PLAY)) ||
|
||||||
|
(stateObj.state === "on" &&
|
||||||
|
(supportsFeature(stateObj, MediaPlayerEntityFeature.PLAY) ||
|
||||||
|
supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE))))
|
||||||
|
) {
|
||||||
|
controls.push({ icon: controlButton.icon, action: controlButton.action });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assumedState) {
|
||||||
|
if (supportsFeature(stateObj, MediaPlayerEntityFeature.PLAY)) {
|
||||||
|
controls.push({ icon: mdiPlay, action: "media_play" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsFeature(stateObj, MediaPlayerEntityFeature.PAUSE)) {
|
||||||
|
controls.push({ icon: mdiPause, action: "media_pause" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsFeature(stateObj, MediaPlayerEntityFeature.STOP)) {
|
||||||
|
controls.push({ icon: mdiStop, action: "media_stop" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(stateObj.state === "playing" || assumedState) &&
|
||||||
|
supportsFeature(stateObj, MediaPlayerEntityFeature.NEXT_TRACK)
|
||||||
|
) {
|
||||||
|
controls.push({ icon: mdiSkipNext, action: "media_next_track" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return controls;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _togglePower(): void {
|
||||||
|
if (!this._stateObj) return;
|
||||||
|
this.hass!.callService(
|
||||||
|
"media_player",
|
||||||
|
stateActive(this._stateObj) ? "turn_off" : "turn_on",
|
||||||
|
{
|
||||||
|
entity_id: this._stateObj.entity_id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _action(e: Event): void {
|
||||||
|
const action = (e.currentTarget as HTMLElement).getAttribute("key");
|
||||||
|
if (!action) return;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "media_play_pause":
|
||||||
|
this._playPauseStop();
|
||||||
|
break;
|
||||||
|
case "media_play":
|
||||||
|
this._play();
|
||||||
|
break;
|
||||||
|
case "media_pause":
|
||||||
|
this._pause();
|
||||||
|
break;
|
||||||
|
case "media_stop":
|
||||||
|
this._stop();
|
||||||
|
break;
|
||||||
|
case "media_previous_track":
|
||||||
|
this._previousTrack();
|
||||||
|
break;
|
||||||
|
case "media_next_track":
|
||||||
|
this._nextTrack();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _playPauseStop(): void {
|
||||||
|
if (!this._stateObj) return;
|
||||||
|
|
||||||
|
const service =
|
||||||
|
this._stateObj.state !== "playing"
|
||||||
|
? "media_play"
|
||||||
|
: supportsFeature(this._stateObj, MediaPlayerEntityFeature.PAUSE)
|
||||||
|
? "media_pause"
|
||||||
|
: "media_stop";
|
||||||
|
|
||||||
|
this.hass!.callService("media_player", service, {
|
||||||
|
entity_id: this._stateObj.entity_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _play(): void {
|
||||||
|
if (!this._stateObj) return;
|
||||||
|
this.hass!.callService("media_player", "media_play", {
|
||||||
|
entity_id: this._stateObj.entity_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _pause(): void {
|
||||||
|
if (!this._stateObj) return;
|
||||||
|
this.hass!.callService("media_player", "media_pause", {
|
||||||
|
entity_id: this._stateObj.entity_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stop(): void {
|
||||||
|
if (!this._stateObj) return;
|
||||||
|
this.hass!.callService("media_player", "media_stop", {
|
||||||
|
entity_id: this._stateObj.entity_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _previousTrack(): void {
|
||||||
|
if (!this._stateObj) return;
|
||||||
|
this.hass!.callService("media_player", "media_previous_track", {
|
||||||
|
entity_id: this._stateObj.entity_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _nextTrack(): void {
|
||||||
|
if (!this._stateObj) return;
|
||||||
|
this.hass!.callService("media_player", "media_next_track", {
|
||||||
|
entity_id: this._stateObj.entity_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = cardFeatureStyles;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-media-player-playback-card-feature": HuiMediaPlayerPlaybackCardFeature;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,10 @@ export interface LockOpenDoorCardFeatureConfig {
|
|||||||
type: "lock-open-door";
|
type: "lock-open-door";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MediaPlayerPlaybackCardFeatureConfig {
|
||||||
|
type: "media-player-playback";
|
||||||
|
}
|
||||||
|
|
||||||
export interface MediaPlayerVolumeSliderCardFeatureConfig {
|
export interface MediaPlayerVolumeSliderCardFeatureConfig {
|
||||||
type: "media-player-volume-slider";
|
type: "media-player-volume-slider";
|
||||||
}
|
}
|
||||||
@@ -242,6 +246,7 @@ export type LovelaceCardFeatureConfig =
|
|||||||
| LightColorTempCardFeatureConfig
|
| LightColorTempCardFeatureConfig
|
||||||
| LockCommandsCardFeatureConfig
|
| LockCommandsCardFeatureConfig
|
||||||
| LockOpenDoorCardFeatureConfig
|
| LockOpenDoorCardFeatureConfig
|
||||||
|
| MediaPlayerPlaybackCardFeatureConfig
|
||||||
| MediaPlayerVolumeSliderCardFeatureConfig
|
| MediaPlayerVolumeSliderCardFeatureConfig
|
||||||
| NumericInputCardFeatureConfig
|
| NumericInputCardFeatureConfig
|
||||||
| SelectOptionsCardFeatureConfig
|
| SelectOptionsCardFeatureConfig
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import "../card-features/hui-light-brightness-card-feature";
|
|||||||
import "../card-features/hui-light-color-temp-card-feature";
|
import "../card-features/hui-light-color-temp-card-feature";
|
||||||
import "../card-features/hui-lock-commands-card-feature";
|
import "../card-features/hui-lock-commands-card-feature";
|
||||||
import "../card-features/hui-lock-open-door-card-feature";
|
import "../card-features/hui-lock-open-door-card-feature";
|
||||||
|
import "../card-features/hui-media-player-playback-card-feature";
|
||||||
import "../card-features/hui-media-player-volume-slider-card-feature";
|
import "../card-features/hui-media-player-volume-slider-card-feature";
|
||||||
import "../card-features/hui-numeric-input-card-feature";
|
import "../card-features/hui-numeric-input-card-feature";
|
||||||
import "../card-features/hui-select-options-card-feature";
|
import "../card-features/hui-select-options-card-feature";
|
||||||
@@ -70,6 +71,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
|
|||||||
"light-color-temp",
|
"light-color-temp",
|
||||||
"lock-commands",
|
"lock-commands",
|
||||||
"lock-open-door",
|
"lock-open-door",
|
||||||
|
"media-player-playback",
|
||||||
"media-player-volume-slider",
|
"media-player-volume-slider",
|
||||||
"numeric-input",
|
"numeric-input",
|
||||||
"select-options",
|
"select-options",
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { supportsLightBrightnessCardFeature } from "../../card-features/hui-ligh
|
|||||||
import { supportsLightColorTempCardFeature } from "../../card-features/hui-light-color-temp-card-feature";
|
import { supportsLightColorTempCardFeature } from "../../card-features/hui-light-color-temp-card-feature";
|
||||||
import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
|
import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
|
||||||
import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature";
|
import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature";
|
||||||
|
import { supportsMediaPlayerPlaybackCardFeature } from "../../card-features/hui-media-player-playback-card-feature";
|
||||||
import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature";
|
import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature";
|
||||||
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
|
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
|
||||||
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
|
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
|
||||||
@@ -95,6 +96,7 @@ const UI_FEATURE_TYPES = [
|
|||||||
"light-color-temp",
|
"light-color-temp",
|
||||||
"lock-commands",
|
"lock-commands",
|
||||||
"lock-open-door",
|
"lock-open-door",
|
||||||
|
"media-player-playback",
|
||||||
"media-player-volume-slider",
|
"media-player-volume-slider",
|
||||||
"numeric-input",
|
"numeric-input",
|
||||||
"select-options",
|
"select-options",
|
||||||
@@ -162,6 +164,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
|
|||||||
"light-color-temp": supportsLightColorTempCardFeature,
|
"light-color-temp": supportsLightColorTempCardFeature,
|
||||||
"lock-commands": supportsLockCommandsCardFeature,
|
"lock-commands": supportsLockCommandsCardFeature,
|
||||||
"lock-open-door": supportsLockOpenDoorCardFeature,
|
"lock-open-door": supportsLockOpenDoorCardFeature,
|
||||||
|
"media-player-playback": supportsMediaPlayerPlaybackCardFeature,
|
||||||
"media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature,
|
"media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature,
|
||||||
"numeric-input": supportsNumericInputCardFeature,
|
"numeric-input": supportsNumericInputCardFeature,
|
||||||
"select-options": supportsSelectOptionsCardFeature,
|
"select-options": supportsSelectOptionsCardFeature,
|
||||||
|
|||||||
@@ -7905,6 +7905,9 @@
|
|||||||
"lock-open-door": {
|
"lock-open-door": {
|
||||||
"label": "Lock open door"
|
"label": "Lock open door"
|
||||||
},
|
},
|
||||||
|
"media-player-playback": {
|
||||||
|
"label": "Media player playback controls"
|
||||||
|
},
|
||||||
"media-player-volume-slider": {
|
"media-player-volume-slider": {
|
||||||
"label": "Media player volume slider"
|
"label": "Media player volume slider"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user