Add support for grouping media players (#24736)
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com> Co-authored-by: Bram Kragten <mail@bramkragten.nl>pull/25592/head
parent
fcf5ed7731
commit
6c7d750734
|
@ -0,0 +1,228 @@
|
||||||
|
import type { CSSResultGroup } from "lit";
|
||||||
|
import { mdiClose } from "@mdi/js";
|
||||||
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import "../ha-alert";
|
||||||
|
import "../ha-dialog";
|
||||||
|
import "../ha-button";
|
||||||
|
import "../ha-dialog-header";
|
||||||
|
import "./ha-media-player-toggle";
|
||||||
|
import type { JoinMediaPlayersDialogParams } from "./show-join-media-players-dialog";
|
||||||
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
|
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||||
|
import {
|
||||||
|
type MediaPlayerEntity,
|
||||||
|
MediaPlayerEntityFeature,
|
||||||
|
mediaPlayerJoin,
|
||||||
|
mediaPlayerUnjoin,
|
||||||
|
} from "../../data/media-player";
|
||||||
|
import { extractApiErrorMessage } from "../../data/hassio/common";
|
||||||
|
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||||
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
|
|
||||||
|
@customElement("dialog-join-media-players")
|
||||||
|
class DialogJoinMediaPlayers extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _entityId?: string;
|
||||||
|
|
||||||
|
@state() private _groupMembers!: string[];
|
||||||
|
|
||||||
|
@state() private _selectedEntities!: string[];
|
||||||
|
|
||||||
|
@state() private _submitting?: boolean;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
public showDialog(params: JoinMediaPlayersDialogParams): void {
|
||||||
|
this._entityId = params.entityId;
|
||||||
|
|
||||||
|
const stateObj = this.hass.states[params.entityId] as
|
||||||
|
| MediaPlayerEntity
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
this._groupMembers =
|
||||||
|
stateObj?.attributes.group_members?.filter(
|
||||||
|
(entityId) => entityId !== params.entityId
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
this._selectedEntities = this._groupMembers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog() {
|
||||||
|
this._entityId = undefined;
|
||||||
|
this._selectedEntities = [];
|
||||||
|
this._groupMembers = [];
|
||||||
|
this._submitting = false;
|
||||||
|
this._error = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._entityId) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityId = this._entityId;
|
||||||
|
const stateObj = this.hass.states[entityId] as HassEntity | undefined;
|
||||||
|
const name = (stateObj && computeStateName(stateObj)) || entityId;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
scrimClickAction
|
||||||
|
escapeKeyAction
|
||||||
|
flexContent
|
||||||
|
.heading=${name}
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
>
|
||||||
|
<ha-dialog-header show-border slot="heading">
|
||||||
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize("ui.common.close")}
|
||||||
|
.path=${mdiClose}
|
||||||
|
dialogAction="close"
|
||||||
|
slot="navigationIcon"
|
||||||
|
></ha-icon-button>
|
||||||
|
<span slot="title"
|
||||||
|
>${this.hass.localize("ui.card.media_player.media_players")}</span
|
||||||
|
>
|
||||||
|
<ha-button slot="actionItems" @click=${this._selectAll}>
|
||||||
|
${this.hass.localize("ui.card.media_player.select_all")}
|
||||||
|
</ha-button>
|
||||||
|
</ha-dialog-header>
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: nothing}
|
||||||
|
<div class="content">
|
||||||
|
<ha-media-player-toggle
|
||||||
|
.hass=${this.hass}
|
||||||
|
.entityId=${entityId}
|
||||||
|
checked
|
||||||
|
disabled
|
||||||
|
></ha-media-player-toggle>
|
||||||
|
${this._mediaPlayerEntities(this.hass.entities).map(
|
||||||
|
(entity) =>
|
||||||
|
html`<ha-media-player-toggle
|
||||||
|
.hass=${this.hass}
|
||||||
|
.entityId=${entity.entity_id}
|
||||||
|
.checked=${this._selectedEntities.includes(entity.entity_id)}
|
||||||
|
@change=${this._handleSelectedChange}
|
||||||
|
></ha-media-player-toggle>`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ha-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||||
|
${this.hass.localize("ui.common.cancel")}
|
||||||
|
</ha-button>
|
||||||
|
<ha-button
|
||||||
|
.disabled=${this._submitting}
|
||||||
|
slot="primaryAction"
|
||||||
|
@click=${this._submit}
|
||||||
|
>
|
||||||
|
${this.hass.localize("ui.common.apply")}
|
||||||
|
</ha-button>
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _mediaPlayerEntities = (
|
||||||
|
entities: Record<string, EntityRegistryDisplayEntry>
|
||||||
|
) => {
|
||||||
|
if (!this._entityId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPlatform = this.hass.entities[this._entityId]?.platform;
|
||||||
|
|
||||||
|
if (!currentPlatform) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(entities).filter((entity) => {
|
||||||
|
if (entity.entity_id === this._entityId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (computeDomain(entity.entity_id) !== "media_player") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.hass.entities[entity.entity_id]?.platform !== currentPlatform) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!this.hass.states[entity.entity_id] ||
|
||||||
|
!supportsFeature(
|
||||||
|
this.hass.states[entity.entity_id],
|
||||||
|
MediaPlayerEntityFeature.GROUPING
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private _selectAll() {
|
||||||
|
this._selectedEntities = this._mediaPlayerEntities(this.hass.entities).map(
|
||||||
|
(entity) => entity.entity_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleSelectedChange(ev) {
|
||||||
|
const selectedEntities = this._selectedEntities.filter(
|
||||||
|
(entityId) => entityId !== ev.target.entityId
|
||||||
|
);
|
||||||
|
if (ev.target.checked) {
|
||||||
|
selectedEntities.push(ev.target.entityId);
|
||||||
|
}
|
||||||
|
this._selectedEntities = selectedEntities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _submit(): Promise<void> {
|
||||||
|
if (!this._entityId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._error = undefined;
|
||||||
|
this._submitting = true;
|
||||||
|
try {
|
||||||
|
// If media is already playing
|
||||||
|
await mediaPlayerJoin(this.hass, this._entityId, this._selectedEntities);
|
||||||
|
await Promise.all(
|
||||||
|
this._groupMembers
|
||||||
|
.filter((entityId) => !this._selectedEntities.includes(entityId))
|
||||||
|
.map((entityId) => mediaPlayerUnjoin(this.hass, entityId))
|
||||||
|
);
|
||||||
|
this.closeDialog();
|
||||||
|
} catch (err) {
|
||||||
|
this._error = extractApiErrorMessage(err);
|
||||||
|
}
|
||||||
|
this._submitting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-dialog-header ha-button {
|
||||||
|
margin: 6px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"dialog-join-media-players": DialogJoinMediaPlayers;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { type CSSResultGroup, LitElement, css, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { mdiSpeaker } from "@mdi/js";
|
||||||
|
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
|
||||||
|
import "../ha-switch";
|
||||||
|
import "../ha-svg-icon";
|
||||||
|
import type { MediaPlayerEntity } from "../../data/media-player";
|
||||||
|
|
||||||
|
@customElement("ha-media-player-toggle")
|
||||||
|
class HaMediaPlayerToggle extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public entityId!: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public checked = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const stateObj = this.hass.states[this.entityId];
|
||||||
|
return html`<div class="list-item">
|
||||||
|
<ha-svg-icon .path=${mdiSpeaker}></ha-svg-icon>
|
||||||
|
<div class="info">
|
||||||
|
<div class="main-text">${computeStateName(stateObj)}</div>
|
||||||
|
<div class="secondary-text">
|
||||||
|
${this._formatSecondaryText(stateObj as MediaPlayerEntity)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ha-switch
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.checked=${this.checked}
|
||||||
|
@change=${this._handleChange}
|
||||||
|
></ha-switch>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _formatSecondaryText(stateObj: MediaPlayerEntity): string {
|
||||||
|
if (stateObj.state !== "playing") {
|
||||||
|
return this.hass.localize("ui.card.media_player.idle");
|
||||||
|
}
|
||||||
|
|
||||||
|
return [stateObj.attributes.media_title, stateObj.attributes.media_artist]
|
||||||
|
.filter((segment) => segment)
|
||||||
|
.join(" · ");
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
.list-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
column-gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-text {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-text[take-height] {
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-text {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleChange(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
this.checked = ev.target.checked;
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-media-player-toggle": HaMediaPlayerToggle;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
|
||||||
|
export interface JoinMediaPlayersDialogParams {
|
||||||
|
entityId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showJoinMediaPlayersDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
dialogParams: JoinMediaPlayersDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "dialog-join-media-players",
|
||||||
|
dialogImport: () => import("./dialog-join-media-players"),
|
||||||
|
dialogParams,
|
||||||
|
});
|
||||||
|
};
|
|
@ -63,6 +63,7 @@ interface MediaPlayerEntityAttributes extends HassEntityAttributeBase {
|
||||||
source_list?: string[];
|
source_list?: string[];
|
||||||
sound_mode?: string;
|
sound_mode?: string;
|
||||||
sound_mode_list?: string[];
|
sound_mode_list?: string[];
|
||||||
|
group_members?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MediaPlayerEntity extends HassEntityBase {
|
export interface MediaPlayerEntity extends HassEntityBase {
|
||||||
|
@ -510,3 +511,12 @@ export const mediaPlayerPlayMedia = (
|
||||||
...extra,
|
...extra,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mediaPlayerJoin = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_id: string,
|
||||||
|
group_members: string[]
|
||||||
|
) => hass.callService("media_player", "join", { group_members }, { entity_id });
|
||||||
|
|
||||||
|
export const mediaPlayerUnjoin = (hass: HomeAssistant, entity_id: string) =>
|
||||||
|
hass.callService("media_player", "unjoin", {}, { entity_id });
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
mdiLoginVariant,
|
mdiLoginVariant,
|
||||||
mdiMusicNote,
|
mdiMusicNote,
|
||||||
mdiPlayBoxMultiple,
|
mdiPlayBoxMultiple,
|
||||||
|
mdiSpeakerMultiple,
|
||||||
mdiVolumeHigh,
|
mdiVolumeHigh,
|
||||||
mdiVolumeMinus,
|
mdiVolumeMinus,
|
||||||
mdiVolumeOff,
|
mdiVolumeOff,
|
||||||
|
@ -20,6 +21,7 @@ import "../../../components/ha-select";
|
||||||
import "../../../components/ha-slider";
|
import "../../../components/ha-slider";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
|
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
|
||||||
|
import { showJoinMediaPlayersDialog } from "../../../components/media-player/show-join-media-players-dialog";
|
||||||
import { isUnavailableState } from "../../../data/entity";
|
import { isUnavailableState } from "../../../data/entity";
|
||||||
import type {
|
import type {
|
||||||
MediaPickedEvent,
|
MediaPickedEvent,
|
||||||
|
@ -46,6 +48,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||||
|
|
||||||
const stateObj = this.stateObj;
|
const stateObj = this.stateObj;
|
||||||
const controls = computeMediaControls(stateObj, true);
|
const controls = computeMediaControls(stateObj, true);
|
||||||
|
const groupMembers = stateObj.attributes.group_members?.length;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
@ -69,7 +72,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||||
${!isUnavailableState(stateObj.state) &&
|
${!isUnavailableState(stateObj.state) &&
|
||||||
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
|
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
|
||||||
? html`
|
? html`
|
||||||
<mwc-button
|
<ha-button
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.card.media_player.browse_media"
|
"ui.card.media_player.browse_media"
|
||||||
)}
|
)}
|
||||||
|
@ -79,7 +82,26 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||||
.path=${mdiPlayBoxMultiple}
|
.path=${mdiPlayBoxMultiple}
|
||||||
slot="icon"
|
slot="icon"
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
</mwc-button>
|
</ha-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${!isUnavailableState(stateObj.state) &&
|
||||||
|
supportsFeature(stateObj, MediaPlayerEntityFeature.GROUPING)
|
||||||
|
? html`
|
||||||
|
<ha-button
|
||||||
|
.label=${this.hass.localize("ui.card.media_player.join")}
|
||||||
|
@click=${this._showGroupMediaPlayers}
|
||||||
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${mdiSpeakerMultiple}
|
||||||
|
slot="icon"
|
||||||
|
></ha-svg-icon>
|
||||||
|
${groupMembers && groupMembers > 1
|
||||||
|
? html`<span class="badge">
|
||||||
|
${stateObj.attributes.group_members?.length}
|
||||||
|
</span>`
|
||||||
|
: nothing}
|
||||||
|
</ha-button>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
|
@ -265,6 +287,23 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||||
mwc-button > ha-svg-icon {
|
mwc-button > ha-svg-icon {
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 16px;
|
||||||
|
min-width: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: var(--ha-font-weight-normal);
|
||||||
|
font-size: var(--ha-font-size-xs);
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
padding: 0 4px;
|
||||||
|
color: var(--text-accent-color, var(--text-primary-color));
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
private _handleClick(e: MouseEvent): void {
|
private _handleClick(e: MouseEvent): void {
|
||||||
|
@ -328,6 +367,12 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _showGroupMediaPlayers(): void {
|
||||||
|
showJoinMediaPlayersDialog(this, {
|
||||||
|
entityId: this.stateObj!.entity_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||||
import type { LinearProgress } from "@material/mwc-linear-progress/mwc-linear-progress";
|
import type { LinearProgress } from "@material/mwc-linear-progress/mwc-linear-progress";
|
||||||
import { mdiDotsVertical, mdiPlayBoxMultiple } from "@mdi/js";
|
import {
|
||||||
|
mdiDotsVertical,
|
||||||
|
mdiPlayBoxMultiple,
|
||||||
|
mdiSpeakerMultiple,
|
||||||
|
} from "@mdi/js";
|
||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
@ -38,6 +42,7 @@ import "../components/hui-marquee";
|
||||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||||
import type { MediaControlCardConfig } from "./types";
|
import type { MediaControlCardConfig } from "./types";
|
||||||
|
import { showJoinMediaPlayersDialog } from "../../../components/media-player/show-join-media-players-dialog";
|
||||||
|
|
||||||
@customElement("hui-media-control-card")
|
@customElement("hui-media-control-card")
|
||||||
export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||||
|
@ -186,6 +191,8 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||||
const mediaDescription = computeMediaDescription(stateObj);
|
const mediaDescription = computeMediaDescription(stateObj);
|
||||||
const mediaTitleClean = cleanupMediaTitle(stateObj.attributes.media_title);
|
const mediaTitleClean = cleanupMediaTitle(stateObj.attributes.media_title);
|
||||||
|
|
||||||
|
const groupMembers = stateObj.attributes.group_members?.length;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card>
|
<ha-card>
|
||||||
<div
|
<div
|
||||||
|
@ -272,34 +279,62 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||||
? ""
|
? ""
|
||||||
: html`
|
: html`
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
${controls!.map(
|
<div class="start">
|
||||||
(control) => html`
|
${controls!.map(
|
||||||
<ha-icon-button
|
(control) => html`
|
||||||
.label=${this.hass.localize(
|
|
||||||
`ui.card.media_player.${control.action}`
|
|
||||||
)}
|
|
||||||
.path=${control.icon}
|
|
||||||
action=${control.action}
|
|
||||||
@click=${this._handleClick}
|
|
||||||
>
|
|
||||||
</ha-icon-button>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
${supportsFeature(
|
|
||||||
stateObj,
|
|
||||||
MediaPlayerEntityFeature.BROWSE_MEDIA
|
|
||||||
)
|
|
||||||
? html`
|
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
class="browse-media"
|
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.card.media_player.browse_media"
|
`ui.card.media_player.${control.action}`
|
||||||
)}
|
)}
|
||||||
.path=${mdiPlayBoxMultiple}
|
.path=${control.icon}
|
||||||
@click=${this._handleBrowseMedia}
|
action=${control.action}
|
||||||
></ha-icon-button>
|
@click=${this._handleClick}
|
||||||
|
>
|
||||||
|
</ha-icon-button>
|
||||||
`
|
`
|
||||||
: ""}
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="end">
|
||||||
|
${supportsFeature(
|
||||||
|
stateObj,
|
||||||
|
MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||||
|
)
|
||||||
|
? html`
|
||||||
|
<ha-icon-button
|
||||||
|
class="browse-media"
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.card.media_player.browse_media"
|
||||||
|
)}
|
||||||
|
.path=${mdiPlayBoxMultiple}
|
||||||
|
@click=${this._handleBrowseMedia}
|
||||||
|
></ha-icon-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${supportsFeature(
|
||||||
|
stateObj,
|
||||||
|
MediaPlayerEntityFeature.GROUPING
|
||||||
|
)
|
||||||
|
? html`
|
||||||
|
<ha-icon-button
|
||||||
|
class="join-media"
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.card.media_player.join"
|
||||||
|
)}
|
||||||
|
@click=${this._handleJoinMediaPlayers}
|
||||||
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${mdiSpeakerMultiple}
|
||||||
|
></ha-svg-icon>
|
||||||
|
${groupMembers && groupMembers > 1
|
||||||
|
? html`<span class="badge">
|
||||||
|
${stateObj.attributes.group_members
|
||||||
|
?.length}
|
||||||
|
</span>`
|
||||||
|
: nothing}
|
||||||
|
</ha-icon-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
|
@ -509,6 +544,12 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleJoinMediaPlayers(): void {
|
||||||
|
showJoinMediaPlayersDialog(this, {
|
||||||
|
entityId: this._config!.entity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private _handleClick(e: MouseEvent): void {
|
private _handleClick(e: MouseEvent): void {
|
||||||
handleMediaControlClick(
|
handleMediaControlClick(
|
||||||
this.hass!,
|
this.hass!,
|
||||||
|
@ -700,6 +741,10 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls > .start {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.controls ha-icon-button {
|
.controls ha-icon-button {
|
||||||
--mdc-icon-button-size: 44px;
|
--mdc-icon-button-size: 44px;
|
||||||
--mdc-icon-size: 30px;
|
--mdc-icon-size: 30px;
|
||||||
|
@ -716,8 +761,12 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon-button.browse-media {
|
ha-icon-button.browse-media {
|
||||||
position: absolute;
|
--mdc-icon-size: 24px;
|
||||||
right: 4px;
|
inset-inline-end: 4px;
|
||||||
|
inset-inline-start: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-icon-button.join-media {
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
inset-inline-end: 4px;
|
inset-inline-end: 4px;
|
||||||
inset-inline-start: initial;
|
inset-inline-start: initial;
|
||||||
|
@ -804,6 +853,25 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||||
.no-progress.player:not(.no-controls) {
|
.no-progress.player:not(.no-controls) {
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 8px;
|
||||||
|
min-height: 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: var(--ha-font-weight-normal);
|
||||||
|
font-size: var(--ha-font-size-xs);
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
padding: 0 4px;
|
||||||
|
color: var(--text-accent-color, var(--text-primary-color));
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -217,7 +217,11 @@
|
||||||
"repeat_set": "Repeat mode",
|
"repeat_set": "Repeat mode",
|
||||||
"shuffle_set": "Shuffle",
|
"shuffle_set": "Shuffle",
|
||||||
"text_to_speak": "Text to speak",
|
"text_to_speak": "Text to speak",
|
||||||
"nothing_playing": "Nothing playing"
|
"nothing_playing": "Nothing playing",
|
||||||
|
"join": "Join",
|
||||||
|
"media_players": "Media players",
|
||||||
|
"select_all": "Select all",
|
||||||
|
"idle": "Idle"
|
||||||
},
|
},
|
||||||
"persistent_notification": {
|
"persistent_notification": {
|
||||||
"dismiss": "Dismiss"
|
"dismiss": "Dismiss"
|
||||||
|
@ -345,6 +349,7 @@
|
||||||
"undo": "Undo",
|
"undo": "Undo",
|
||||||
"move": "Move",
|
"move": "Move",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
"apply": "Apply",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
|
|
Loading…
Reference in New Issue