From 7be2c59295418b6541a0775d164e284fdb370b01 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 24 Sep 2025 16:13:37 +0200 Subject: [PATCH] Redesign media player more-info dialog (#26904) * Redesign media player more-info dialog * Add missing imports * Add some more media player controls to gallery * Fix NaN * Fix first example source * Regroup * Remove * Add marquee text * Buttons * aria-label * Increase speed * Improve marquee text * Improve marquee text * Improve marquee text * Add touch events to marquee text * Use classMap * Remove chip styling * Make ha-marquee-text slotted and add to gallery * Format * Remove aria-label * Make turn on and off buttons have labels * Match more figma * Add integration logo and move grouping/inputs to top * Hm * Fix badge * Minor tweaks * Disable position slider when seek is not supported * Process code review * remove disabled color for slider * Process UX * Run prettier * Mark listener as passive * Improve bottom controls and styling * Remove unused function * Some minor improvements * Show remaining instead duration --------- Co-authored-by: Paulus Schoutsen --- gallery/src/data/media_players.ts | 14 +- .../pages/components/ha-marquee-text.markdown | 37 + .../src/pages/components/ha-marquee-text.ts | 25 + src/components/ha-marquee-text.ts | 178 ++++ src/dialogs/more-info/const.ts | 1 + .../controls/more-info-media_player.ts | 758 ++++++++++++------ src/fake_data/provide_hass.ts | 12 + src/translations/en.json | 3 +- 8 files changed, 800 insertions(+), 228 deletions(-) create mode 100644 gallery/src/pages/components/ha-marquee-text.markdown create mode 100644 gallery/src/pages/components/ha-marquee-text.ts create mode 100644 src/components/ha-marquee-text.ts diff --git a/gallery/src/data/media_players.ts b/gallery/src/data/media_players.ts index 1d343c5bad..bd7c332173 100644 --- a/gallery/src/data/media_players.ts +++ b/gallery/src/data/media_players.ts @@ -17,6 +17,10 @@ export const createMediaPlayerEntities = () => [ new Date().getTime() - 23000 ).toISOString(), volume_level: 0.5, + source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"], + source: "AirPlay", + sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"], + sound_mode: "Music", }), getEntity("media_player", "music_playing", "playing", { friendly_name: "Playing The Music", @@ -24,8 +28,8 @@ export const createMediaPlayerEntities = () => [ media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", media_artist: "Technohead", // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + - // Select Source + Stop + Clear + Play + Shuffle Set + Browse Media - supported_features: 195135, + // Select Source + Stop + Clear + Play + Shuffle Set + Browse Media + Grouping + supported_features: 784959, entity_picture: "/images/album_cover.jpg", media_duration: 300, media_position: 0, @@ -34,6 +38,9 @@ export const createMediaPlayerEntities = () => [ new Date().getTime() - 23000 ).toISOString(), volume_level: 0.5, + sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"], + sound_mode: "Music", + group_members: ["media_player.playing", "media_player.stream_playing"], }), getEntity("media_player", "stream_playing", "playing", { friendly_name: "Playing the Stream", @@ -149,15 +156,18 @@ export const createMediaPlayerEntities = () => [ }), getEntity("media_player", "receiver_on", "on", { source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"], + sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"], volume_level: 0.63, is_volume_muted: false, source: "TV", + sound_mode: "Movie", friendly_name: "Receiver (selectable sources)", // Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode supported_features: 84364, }), getEntity("media_player", "receiver_off", "off", { source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"], + sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"], friendly_name: "Receiver (selectable sources)", // Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode supported_features: 84364, diff --git a/gallery/src/pages/components/ha-marquee-text.markdown b/gallery/src/pages/components/ha-marquee-text.markdown new file mode 100644 index 0000000000..585e888bd4 --- /dev/null +++ b/gallery/src/pages/components/ha-marquee-text.markdown @@ -0,0 +1,37 @@ +--- +title: Marquee Text +--- + +# Marquee Text `` + +Marquee text component scrolls text horizontally if it overflows its container. It supports pausing on hover and customizable speed and pause duration. + +## Implementation + +### Example Usage + + + This is a long text that will scroll horizontally if it overflows the container. + + +```html + + This is a long text that will scroll horizontally if it overflows the + container. + +``` + +### API + +**Slots** + +- default slot: The text content to be displayed and scrolled. + - no default + +**Properties/Attributes** + +| Name | Type | Default | Description | +| -------------- | ------- | ------- | ---------------------------------------------------------------------------- | +| speed | number | `15` | The speed of the scrolling animation. Higher values result in faster scroll. | +| pause-on-hover | boolean | `true` | Whether to pause the scrolling animation when | +| pause-duration | number | `1000` | The delay in milliseconds before the scrolling animation starts/restarts. | diff --git a/gallery/src/pages/components/ha-marquee-text.ts b/gallery/src/pages/components/ha-marquee-text.ts new file mode 100644 index 0000000000..b9ef4ec713 --- /dev/null +++ b/gallery/src/pages/components/ha-marquee-text.ts @@ -0,0 +1,25 @@ +import { css, LitElement } from "lit"; +import { customElement } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import "../../../../src/components/ha-marquee-text"; + +@customElement("demo-components-ha-marquee-text") +export class DemoHaMarqueeText extends LitElement { + static styles = css` + ha-card { + max-width: 600px; + margin: 24px auto; + } + .card-content { + display: flex; + flex-direction: column; + align-items: flex-start; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "demo-components-ha-marquee-text": DemoHaMarqueeText; + } +} diff --git a/src/components/ha-marquee-text.ts b/src/components/ha-marquee-text.ts new file mode 100644 index 0000000000..a74175c5cc --- /dev/null +++ b/src/components/ha-marquee-text.ts @@ -0,0 +1,178 @@ +import { + type TemplateResult, + LitElement, + html, + css, + type PropertyValues, +} from "lit"; +import { customElement, eventOptions, property, query } from "lit/decorators"; + +@customElement("ha-marquee-text") +export class HaMarqueeText extends LitElement { + @property({ type: Number }) speed = 15; // pixels per second + + @property({ type: Number, attribute: "pause-duration" }) pauseDuration = 1000; // ms delay at ends + + @property({ type: Boolean, attribute: "pause-on-hover" }) + pauseOnHover = false; + + private _direction: "left" | "right" = "left"; + + private _animationFrame?: number; + + @query(".marquee-container") + private _container?: HTMLDivElement; + + @query(".marquee-text") + private _textSpan?: HTMLSpanElement; + + private _position = 0; + + private _maxOffset = 0; + + private _pauseTimeout?: number; + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + + this._setupAnimation(); + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + + if (changedProps.has("text")) { + this._setupAnimation(); + } + } + + public disconnectedCallback() { + super.disconnectedCallback(); + + if (this._animationFrame) { + cancelAnimationFrame(this._animationFrame); + } + if (this._pauseTimeout) { + clearTimeout(this._pauseTimeout); + this._pauseTimeout = undefined; + } + } + + protected render(): TemplateResult { + return html` +
+ +
+ `; + } + + private _setupAnimation() { + if (!this._container || !this._textSpan) { + return; + } + + this._position = 0; + this._direction = "left"; + this._maxOffset = Math.max( + 0, + this._textSpan.offsetWidth - this._container.offsetWidth + ); + this._textSpan.style.transform = `translateX(0px)`; + if (this._animationFrame) { + cancelAnimationFrame(this._animationFrame); + } + if (this._pauseTimeout) { + clearTimeout(this._pauseTimeout); + this._pauseTimeout = undefined; + } + this._animate(); + } + + private _animate = () => { + if (!this._container || !this._textSpan) { + return; + } + + const dt = 1 / 60; // ~16ms per frame + const pxPerFrame = this.speed * dt; + let reachedEnd = false; + if (this._direction === "left") { + this._position -= pxPerFrame; + if (this._position <= -this._maxOffset) { + this._position = -this._maxOffset; + this._direction = "right"; + reachedEnd = true; + } + } else { + this._position += pxPerFrame; + if (this._position >= 0) { + this._position = 0; + this._direction = "left"; + reachedEnd = true; + } + } + this._textSpan.style.transform = `translateX(${this._position}px)`; + if (reachedEnd) { + this._pauseTimeout = window.setTimeout(() => { + this._pauseTimeout = undefined; + this._animationFrame = requestAnimationFrame(this._animate); + }, this.pauseDuration); + } else { + this._animationFrame = requestAnimationFrame(this._animate); + } + }; + + @eventOptions({ passive: true }) + private _handleMouseEnter() { + if (this.pauseOnHover && this._animationFrame) { + cancelAnimationFrame(this._animationFrame); + this._animationFrame = undefined; + } + if (this.pauseOnHover && this._pauseTimeout) { + clearTimeout(this._pauseTimeout); + this._pauseTimeout = undefined; + } + } + + private _handleMouseLeave() { + if (this.pauseOnHover && !this._animationFrame && !this._pauseTimeout) { + this._animate(); + } + } + + static styles = css` + :host { + display: block; + overflow: hidden; + width: 100%; + } + + .marquee-container { + width: 100%; + white-space: nowrap; + overflow: hidden; + user-select: none; + cursor: default; + } + + .marquee-text { + display: inline-block; + vertical-align: middle; + will-change: transform; + font-size: 1em; + pointer-events: none; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-marquee-text": HaMarqueeText; + } +} diff --git a/src/dialogs/more-info/const.ts b/src/dialogs/more-info/const.ts index cdcf2bc94a..8e27ef1340 100644 --- a/src/dialogs/more-info/const.ts +++ b/src/dialogs/more-info/const.ts @@ -34,6 +34,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [ "valve", "water_heater", "weather", + "media_player", ]; /** Domains with full height more info dialog */ export const DOMAINS_FULL_HEIGHT_MORE_INFO = ["update"]; diff --git a/src/dialogs/more-info/controls/more-info-media_player.ts b/src/dialogs/more-info/controls/more-info-media_player.ts index 54241a7630..99025e7dce 100644 --- a/src/dialogs/more-info/controls/more-info-media_player.ts +++ b/src/dialogs/more-info/controls/more-info-media_player.ts @@ -1,6 +1,7 @@ import { mdiLoginVariant, mdiMusicNote, + mdiMusicNoteEighth, mdiPlayBoxMultiple, mdiSpeakerMultiple, mdiVolumeHigh, @@ -8,11 +9,13 @@ import { mdiVolumeOff, mdiVolumePlus, } from "@mdi/js"; -import { LitElement, css, html, nothing } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; +import { ifDefined } from "lit/directives/if-defined"; +import { classMap } from "lit/directives/class-map"; import { stateActive } from "../../../common/entity/state_active"; import { supportsFeature } from "../../../common/entity/supports-feature"; +import { formatDurationDigital } from "../../../common/datetime/format_duration"; import "../../../components/ha-icon-button"; import "../../../components/ha-list-item"; import "../../../components/ha-select"; @@ -27,12 +30,17 @@ import type { MediaPlayerEntity, } from "../../../data/media-player"; import { - MediaPlayerEntityFeature, computeMediaControls, handleMediaControlClick, + MediaPlayerEntityFeature, mediaPlayerPlayMedia, } from "../../../data/media-player"; import type { HomeAssistant } from "../../../types"; +import HassMediaPlayerEntity from "../../../util/hass-media-player-model"; +import "../../../components/ha-md-button-menu"; +import "../../../components/chips/ha-assist-chip"; +import "../../../components/ha-md-menu-item"; +import "../../../components/ha-marquee-text"; @customElement("more-info-media_player") class MoreInfoMediaPlayer extends LitElement { @@ -40,259 +48,488 @@ class MoreInfoMediaPlayer extends LitElement { @property({ attribute: false }) public stateObj?: MediaPlayerEntity; + private _formateDuration(duration: number) { + const hours = Math.floor(duration / 3600); + const minutes = Math.floor((duration % 3600) / 60); + const seconds = duration % 60; + return formatDurationDigital(this.hass.locale, { + hours, + minutes, + seconds, + })!; + } + + protected _renderVolumeControl() { + if (!this.stateObj) { + return nothing; + } + + const supportsMute = supportsFeature( + this.stateObj, + MediaPlayerEntityFeature.VOLUME_MUTE + ); + const supportsSliding = supportsFeature( + this.stateObj, + MediaPlayerEntityFeature.VOLUME_SET + ); + + return html`${(supportsFeature( + this.stateObj!, + MediaPlayerEntityFeature.VOLUME_SET + ) || + supportsFeature(this.stateObj!, MediaPlayerEntityFeature.VOLUME_STEP)) && + stateActive(this.stateObj!) + ? html` +
+ ${supportsMute + ? html` + + ` + : ""} + ${supportsFeature( + this.stateObj, + MediaPlayerEntityFeature.VOLUME_STEP + ) && !supportsSliding + ? html` + + + ` + : nothing} + ${supportsSliding + ? html` + ${!supportsMute + ? html`` + : nothing} + + ` + : nothing} +
+ ` + : nothing}`; + } + + protected _renderSourceControl() { + if ( + !this.stateObj || + !supportsFeature(this.stateObj, MediaPlayerEntityFeature.SELECT_SOURCE) || + !this.stateObj.attributes.source_list?.length + ) { + return nothing; + } + + return html` + + + + ${this.stateObj.attributes.source_list!.map( + (source) => + html` + ${source} + ` + )} + `; + } + + protected _renderSoundMode() { + if ( + !this.stateObj || + !supportsFeature( + this.stateObj, + MediaPlayerEntityFeature.SELECT_SOUND_MODE + ) || + !this.stateObj.attributes.sound_mode_list?.length + ) { + return nothing; + } + + return html` + + + + ${this.stateObj.attributes.sound_mode_list!.map( + (soundMode) => + html` + ${soundMode} + ` + )} + `; + } + + protected _renderGrouping() { + if ( + !this.stateObj || + isUnavailableState(this.stateObj.state) || + !supportsFeature(this.stateObj, MediaPlayerEntityFeature.GROUPING) + ) { + return nothing; + } + const groupMembers = this.stateObj.attributes.group_members; + const hasMultipleMembers = groupMembers && groupMembers?.length > 1; + + return html` +
+ + ${hasMultipleMembers + ? html` ${groupMembers?.length || 4} ` + : nothing} +
+
`; + } + + protected _renderEmptyCover(title: string, icon?: string) { + return html` +
+ +
+ `; + } + protected render() { if (!this.stateObj) { return nothing; } + if (isUnavailableState(this.stateObj.state)) { + return this._renderEmptyCover(this.hass.formatEntityState(this.stateObj)); + } + const stateObj = this.stateObj; const controls = computeMediaControls(stateObj, true); - const groupMembers = stateObj.attributes.group_members?.length; + const coverUrl = stateObj.attributes.entity_picture || ""; + const playerObj = new HassMediaPlayerEntity(this.hass, this.stateObj); + const position = Math.floor(playerObj.currentProgress) || 0; + const duration = stateObj.attributes.media_duration || 0; + const remaining = duration - position; + const durationFormated = + remaining > 0 ? this._formateDuration(remaining) : 0; + const postionFormated = this._formateDuration(position); + const primaryTitle = playerObj.primaryTitle; + const secondaryTitle = playerObj.secondaryTitle; + const turnOn = controls?.find((c) => c.action === "turn_on"); + const turnOff = controls?.find((c) => c.action === "turn_off"); return html` -
-
- ${!controls - ? "" - : controls.map( - (control) => html` - - - ` - )} -
- ${!isUnavailableState(stateObj.state) && - supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA) - ? html` - - - ${this.hass.localize("ui.card.media_player.browse_media")} - - ` - : ""} - ${!isUnavailableState(stateObj.state) && - supportsFeature(stateObj, MediaPlayerEntityFeature.GROUPING) - ? html` - - - ${groupMembers && groupMembers > 1 - ? html` - ${stateObj.attributes.group_members?.length || 4} - ` - : nothing} - ${this.hass.localize("ui.card.media_player.join")} - - ` - : ""} -
- ${(supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET) || - supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_STEP)) && - stateActive(stateObj) + ${coverUrl + ? html`
+ ${ifDefined(primaryTitle)} +
` + : this._renderEmptyCover( + this.hass.formatEntityState(this.stateObj), + mdiMusicNote + )} + ${primaryTitle || secondaryTitle + ? html`
+ ${primaryTitle + ? html` + ${primaryTitle} + ` + : nothing} + ${secondaryTitle + ? html` + ${secondaryTitle} + ` + : nothing} +
` + : nothing} + ${duration && duration > 0 ? html` -
- ${supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_MUTE) - ? html` - - ` - : ""} - ${supportsFeature( - stateObj, - MediaPlayerEntityFeature.VOLUME_SET - ) || - supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_STEP) - ? html` - - - ` - : ""} - ${supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET) - ? html` - - ` - : ""} -
- ` - : ""} - ${stateActive(stateObj) && - supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOURCE) && - stateObj.attributes.source_list?.length - ? html` -
- - ${stateObj.attributes.source_list!.map( - (source) => html` - - ${this.hass.formatEntityAttributeValue( - stateObj, - "source", - source - )} - - ` +
+ - + @change=${this._handleMediaSeekChanged} + ?disabled=${!stateActive(stateObj) || + !supportsFeature(stateObj, MediaPlayerEntityFeature.SEEK)} + > +
+ ${postionFormated} + ${durationFormated} +
` : nothing} - ${stateActive(stateObj) && - supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOUND_MODE) && - stateObj.attributes.sound_mode_list?.length - ? html` -
- - ${stateObj.attributes.sound_mode_list.map( - (mode) => html` - - ${this.hass.formatEntityAttributeValue( - stateObj, - "sound_mode", - mode +
+ ${controls && controls.length > 0 + ? html`
+ ${["repeat_set", "media_previous_track"].map((action) => { + const control = controls?.find((c) => c.action === action); + return control + ? html` - ` + > + ` + : html``; + })} + ${["media_play_pause", "media_pause", "media_play"].map( + (action) => { + const control = controls?.find((c) => c.action === action); + return control + ? html` + + ` + : nothing; + } + )} + ${["media_next_track", "shuffle_set"].map((action) => { + const control = controls?.find((c) => c.action === action); + return control + ? html` + ` + : html``; + })} +
` + : nothing} + ${this._renderVolumeControl()} +
+ ${!isUnavailableState(stateObj.state) && + supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA) + ? html` + + + + ` + : nothing} + ${this._renderGrouping()} ${this._renderSourceControl()} + ${this._renderSoundMode()} + ${turnOn + ? html` - -
- ` - : ""} + > + + ` + : nothing} + ${turnOff + ? html` + + ` + : nothing} +
+
`; } static styles = css` - ha-slider { - flex-grow: 1; - } - - ha-icon-button[action="turn_off"], - ha-icon-button[action="turn_on"] { - margin-right: auto; - margin-left: inherit; - margin-inline-start: inherit; - margin-inline-end: auto; - } - - .controls { + :host { display: flex; - flex-wrap: wrap; + flex-direction: column; + gap: 24px; + margin-top: 0; + } + + .cover-container { + display: flex; + justify-content: center; align-items: center; - --mdc-theme-primary: currentColor; - direction: ltr; + height: 320px; + width: 100%; } - .basic-controls { - display: inline-flex; - flex-grow: 1; + .cover-image { + width: 240px; + height: 240px; + max-width: 100%; + max-height: 100%; + object-fit: cover; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + position: relative; + display: flex; + align-items: center; + justify-content: center; + transition: + width 0.3s, + height 0.3s; } - .volume { - direction: ltr; + .cover-image--playing { + width: 320px; + height: 320px; } - .source-input, - .sound-input { - direction: var(--direction); + .empty-cover { + background-color: var(--secondary-background-color); + font-size: 1.5em; + color: var(--secondary-text-color); } - .volume, - .source-input, - .sound-input { + .main-controls { display: flex; align-items: center; justify-content: space-between; } - .source-input ha-select, - .sound-input ha-select { - margin-left: 10px; - flex-grow: 1; - margin-inline-start: 10px; - margin-inline-end: initial; - direction: var(--direction); + .center-control { + --ha-button-height: 56px; } - .tts { - margin-top: 16px; - font-style: italic; + .spacer { + width: 48px; } - ha-button > ha-svg-icon { - vertical-align: text-bottom; + .volume, + .position-bar, + .main-controls { + direction: ltr; + } + + .volume ha-slider, + .position-bar ha-slider { + width: 100%; + } + + .volume { + display: flex; + align-items: center; + gap: 12px; + margin-left: 8px; + } + + .volume ha-svg-icon { + padding: 4px; + height: 16px; + width: 16px; + } + + .volume ha-icon-button { + --mdc-icon-button-size: 32px; + --mdc-icon-size: 16px; } .badge { position: absolute; - top: -6px; - left: 24px; + top: -10px; + left: 16px; display: flex; justify-content: center; align-items: center; @@ -301,9 +538,68 @@ class MoreInfoMediaPlayer extends LitElement { border-radius: 10px; font-weight: var(--ha-font-weight-normal); font-size: var(--ha-font-size-xs); - background-color: var(--accent-color); + background-color: var(--primary-color); padding: 0 4px; - color: var(--text-accent-color, var(--text-primary-color)); + color: var(--primary-text-color); + } + + .position-bar { + display: flex; + flex-direction: column; + } + + .position-info-row { + display: flex; + flex-direction: row; + justify-content: space-between; + color: var(--secondary-text-color); + padding: 0 8px; + font-size: var(--ha-font-size-s); + } + + .media-info-row { + display: flex; + flex-direction: column; + justify-content: space-between; + margin: 8px 0 8px 8px; + } + + .media-title { + font-size: var(--ha-font-size-xl); + font-weight: var(--ha-font-weight-bold); + margin-bottom: 4px; + } + + .media-artist { + font-size: var(--ha-font-size-l); + font-weight: var(--ha-font-weight-normal); + color: var(--secondary-text-color); + } + + .controls-row { + display: flex; + align-items: center; + justify-content: space-around; + } + + .controls-row ha-button { + width: 32px; + } + + .controls-row ha-svg-icon { + color: var(--ha-color-on-neutral-quiet); + } + + .grouping::part(label) { + position: relative; + } + + .bottom-controls { + display: flex; + flex-direction: column; + gap: 24px; + align-self: center; + width: 320px; } `; @@ -329,29 +625,29 @@ class MoreInfoMediaPlayer extends LitElement { }); } - private _handleSourceChanged(e) { - const newVal = e.target.value; - - if (!newVal || this.stateObj!.attributes.source === newVal) { + private _handleSourceClick(e: Event) { + const source = (e.currentTarget as HTMLElement).getAttribute("data-source"); + if (!source || this.stateObj!.attributes.source === source) { return; } this.hass.callService("media_player", "select_source", { entity_id: this.stateObj!.entity_id, - source: newVal, + source, }); } - private _handleSoundModeChanged(e) { - const newVal = e.target.value; - - if (!newVal || this.stateObj?.attributes.sound_mode === newVal) { + private _handleSoundModeClick(e: Event) { + const soundMode = (e.currentTarget as HTMLElement).getAttribute( + "data-sound-mode" + ); + if (!soundMode || this.stateObj!.attributes.sound_mode === soundMode) { return; } this.hass.callService("media_player", "select_sound_mode", { entity_id: this.stateObj!.entity_id, - sound_mode: newVal, + sound_mode: soundMode, }); } @@ -374,6 +670,18 @@ class MoreInfoMediaPlayer extends LitElement { entityId: this.stateObj!.entity_id, }); } + + private async _handleMediaSeekChanged(e: Event): Promise { + if (!this.stateObj) { + return; + } + + const newValue = (e.target as any).value; + this.hass.callService("media_player", "media_seek", { + entity_id: this.stateObj.entity_id, + seek_position: newValue, + }); + } } declare global { diff --git a/src/fake_data/provide_hass.ts b/src/fake_data/provide_hass.ts index 24864d8b8b..99020c3285 100644 --- a/src/fake_data/provide_hass.ts +++ b/src/fake_data/provide_hass.ts @@ -22,6 +22,7 @@ import { demoPanels } from "./demo_panels"; import { demoServices } from "./demo_services"; import type { Entity } from "./entity"; import { getEntity } from "./entity"; +import type { EntityRegistryDisplayEntry } from "../data/entity_registry"; const ensureArray = (val: T | T[]): T[] => Array.isArray(val) ? val : [val]; @@ -147,6 +148,17 @@ export const provideHass = ( } else { updateStates(states); } + + for (const ent of ensureArray(newEntities)) { + hass().entities[ent.entityId] = { + entity_id: ent.entityId, + name: ent.name, + icon: ent.icon, + platform: "demo", + labels: [], + } satisfies EntityRegistryDisplayEntry; + } + updateFormatFunctions(); } diff --git a/src/translations/en.json b/src/translations/en.json index 3158c7c657..e1191c2c9b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -233,7 +233,8 @@ "join": "Join", "media_players": "Media players", "select_all": "Select all", - "idle": "Idle" + "idle": "Idle", + "track_position": "Track position" }, "persistent_notification": { "dismiss": "Dismiss"