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[];
|
||||
sound_mode?: string;
|
||||
sound_mode_list?: string[];
|
||||
group_members?: string[];
|
||||
}
|
||||
|
||||
export interface MediaPlayerEntity extends HassEntityBase {
|
||||
|
@ -510,3 +511,12 @@ export const mediaPlayerPlayMedia = (
|
|||
...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,
|
||||
mdiMusicNote,
|
||||
mdiPlayBoxMultiple,
|
||||
mdiSpeakerMultiple,
|
||||
mdiVolumeHigh,
|
||||
mdiVolumeMinus,
|
||||
mdiVolumeOff,
|
||||
|
@ -20,6 +21,7 @@ import "../../../components/ha-select";
|
|||
import "../../../components/ha-slider";
|
||||
import "../../../components/ha-svg-icon";
|
||||
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 type {
|
||||
MediaPickedEvent,
|
||||
|
@ -46,6 +48,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
|||
|
||||
const stateObj = this.stateObj;
|
||||
const controls = computeMediaControls(stateObj, true);
|
||||
const groupMembers = stateObj.attributes.group_members?.length;
|
||||
|
||||
return html`
|
||||
<div class="controls">
|
||||
|
@ -69,7 +72,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
|||
${!isUnavailableState(stateObj.state) &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
|
||||
? html`
|
||||
<mwc-button
|
||||
<ha-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.browse_media"
|
||||
)}
|
||||
|
@ -79,7 +82,26 @@ class MoreInfoMediaPlayer extends LitElement {
|
|||
.path=${mdiPlayBoxMultiple}
|
||||
slot="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>
|
||||
|
@ -265,6 +287,23 @@ class MoreInfoMediaPlayer extends LitElement {
|
|||
mwc-button > ha-svg-icon {
|
||||
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 {
|
||||
|
@ -328,6 +367,12 @@ class MoreInfoMediaPlayer extends LitElement {
|
|||
),
|
||||
});
|
||||
}
|
||||
|
||||
private _showGroupMediaPlayers(): void {
|
||||
showJoinMediaPlayersDialog(this, {
|
||||
entityId: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import "@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 { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
|
@ -38,6 +42,7 @@ import "../components/hui-marquee";
|
|||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import type { MediaControlCardConfig } from "./types";
|
||||
import { showJoinMediaPlayersDialog } from "../../../components/media-player/show-join-media-players-dialog";
|
||||
|
||||
@customElement("hui-media-control-card")
|
||||
export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
|
@ -186,6 +191,8 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
|||
const mediaDescription = computeMediaDescription(stateObj);
|
||||
const mediaTitleClean = cleanupMediaTitle(stateObj.attributes.media_title);
|
||||
|
||||
const groupMembers = stateObj.attributes.group_members?.length;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div
|
||||
|
@ -272,34 +279,62 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
|||
? ""
|
||||
: html`
|
||||
<div class="controls">
|
||||
${controls!.map(
|
||||
(control) => html`
|
||||
<ha-icon-button
|
||||
.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`
|
||||
<div class="start">
|
||||
${controls!.map(
|
||||
(control) => html`
|
||||
<ha-icon-button
|
||||
class="browse-media"
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.browse_media"
|
||||
`ui.card.media_player.${control.action}`
|
||||
)}
|
||||
.path=${mdiPlayBoxMultiple}
|
||||
@click=${this._handleBrowseMedia}
|
||||
></ha-icon-button>
|
||||
.path=${control.icon}
|
||||
action=${control.action}
|
||||
@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>
|
||||
|
@ -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 {
|
||||
handleMediaControlClick(
|
||||
this.hass!,
|
||||
|
@ -700,6 +741,10 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.controls > .start {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.controls ha-icon-button {
|
||||
--mdc-icon-button-size: 44px;
|
||||
--mdc-icon-size: 30px;
|
||||
|
@ -716,8 +761,12 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
|||
}
|
||||
|
||||
ha-icon-button.browse-media {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
--mdc-icon-size: 24px;
|
||||
inset-inline-end: 4px;
|
||||
inset-inline-start: initial;
|
||||
}
|
||||
|
||||
ha-icon-button.join-media {
|
||||
--mdc-icon-size: 24px;
|
||||
inset-inline-end: 4px;
|
||||
inset-inline-start: initial;
|
||||
|
@ -804,6 +853,25 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
|||
.no-progress.player:not(.no-controls) {
|
||||
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",
|
||||
"shuffle_set": "Shuffle",
|
||||
"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": {
|
||||
"dismiss": "Dismiss"
|
||||
|
@ -345,6 +349,7 @@
|
|||
"undo": "Undo",
|
||||
"move": "Move",
|
||||
"save": "Save",
|
||||
"apply": "Apply",
|
||||
"add": "Add",
|
||||
"create": "Create",
|
||||
"edit": "Edit",
|
||||
|
|
Loading…
Reference in New Issue