1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-02-15 07:25:54 +00:00

Media player fixes (#29075)

* aligning ui of dialog and media bar

* refactored media progress logic to be reusable

* updating track times to be consistent with music assistant

* WIP aligning volume slider with music assistant

* migrating to ha-dropdown

* showing volume tooltip on touch devices

* Fixed volume slider going to 100 randomly

* Added scrolling support

* Refactored volume control logic
This commit is contained in:
uptimeZERO_
2026-01-21 19:28:32 +00:00
committed by GitHub
parent fd506d4d72
commit ea1b7b9dec
5 changed files with 535 additions and 104 deletions

View File

@@ -0,0 +1,19 @@
export const startMediaProgressInterval = (
interval: number | undefined,
callback: () => void,
intervalMs = 1000
): number => {
if (interval) {
return interval;
}
return window.setInterval(callback, intervalMs);
};
export const stopMediaProgressInterval = (
interval: number | undefined
): number | undefined => {
if (interval) {
clearInterval(interval);
}
return undefined;
};

View File

@@ -0,0 +1,186 @@
import type { HaSlider } from "../../components/ha-slider";
interface VolumeSliderControllerOptions {
getSlider: () => HaSlider | undefined;
step: number;
onSetVolume: (value: number) => void;
onSetVolumeDebounced?: (value: number) => void;
onValueUpdated?: (value: number) => void;
}
export class VolumeSliderController {
private _touchStartX = 0;
private _touchStartY = 0;
private _touchStartValue = 0;
private _touchDragging = false;
private _touchScrolling = false;
private _dragging = false;
private _lastValue = 0;
private _options: VolumeSliderControllerOptions;
constructor(options: VolumeSliderControllerOptions) {
this._options = options;
}
public get isInteracting(): boolean {
return this._touchDragging || this._dragging;
}
public setStep(step: number): void {
this._options.step = step;
}
public handleInput = (ev: Event): void => {
ev.stopPropagation();
const value = Number((ev.target as HaSlider).value);
this._dragging = true;
this._updateValue(value);
this._options.onSetVolumeDebounced?.(value);
};
public handleChange = (ev: Event): void => {
ev.stopPropagation();
const value = Number((ev.target as HaSlider).value);
this._dragging = false;
this._updateValue(value);
this._options.onSetVolume(value);
};
public handleTouchStart = (ev: TouchEvent): void => {
ev.stopPropagation();
const touch = ev.touches[0];
this._touchStartX = touch.clientX;
this._touchStartY = touch.clientY;
this._touchStartValue = this._getSliderValue();
this._touchDragging = false;
this._touchScrolling = false;
this._showTooltip();
};
public handleTouchMove = (ev: TouchEvent): void => {
if (this._touchScrolling) {
return;
}
const touch = ev.touches[0];
const deltaX = touch.clientX - this._touchStartX;
const deltaY = touch.clientY - this._touchStartY;
const absDeltaX = Math.abs(deltaX);
const absDeltaY = Math.abs(deltaY);
if (!this._touchDragging) {
if (absDeltaY > 10 && absDeltaY > absDeltaX * 2) {
this._touchScrolling = true;
return;
}
if (absDeltaX > 8) {
this._touchDragging = true;
}
}
if (this._touchDragging) {
ev.preventDefault();
const newValue = this._getVolumeFromTouch(touch.clientX);
this._updateValue(newValue);
}
};
public handleTouchEnd = (ev: TouchEvent): void => {
if (this._touchScrolling) {
this._touchScrolling = false;
this._hideTooltip();
return;
}
const touch = ev.changedTouches[0];
if (!this._touchDragging) {
const tapValue = this._getVolumeFromTouch(touch.clientX);
const delta =
tapValue > this._touchStartValue
? this._options.step
: -this._options.step;
const newValue = this._roundVolumeValue(this._touchStartValue + delta);
this._updateValue(newValue);
this._options.onSetVolume(newValue);
} else {
const finalValue = this._getVolumeFromTouch(touch.clientX);
this._updateValue(finalValue);
this._options.onSetVolume(finalValue);
}
this._touchDragging = false;
this._dragging = false;
this._hideTooltip();
};
public handleTouchCancel = (): void => {
this._touchDragging = false;
this._touchScrolling = false;
this._dragging = false;
this._updateValue(this._touchStartValue);
this._hideTooltip();
};
public handleWheel = (ev: WheelEvent): void => {
ev.preventDefault();
ev.stopPropagation();
const direction = ev.deltaY > 0 ? -1 : 1;
const currentValue = this._getSliderValue();
const newValue = this._roundVolumeValue(
currentValue + direction * this._options.step
);
this._updateValue(newValue);
this._options.onSetVolume(newValue);
};
private _getVolumeFromTouch(clientX: number): number {
const slider = this._options.getSlider();
if (!slider) {
return 0;
}
const rect = slider.getBoundingClientRect();
const x = Math.min(Math.max(clientX - rect.left, 0), rect.width);
const percentage = (x / rect.width) * 100;
return this._roundVolumeValue(percentage);
}
private _roundVolumeValue(value: number): number {
return Math.min(
Math.max(Math.round(value / this._options.step) * this._options.step, 0),
100
);
}
private _getSliderValue(): number {
const slider = this._options.getSlider();
if (slider) {
return Number(slider.value);
}
return this._lastValue;
}
private _updateValue(value: number): void {
this._lastValue = value;
this._options.onValueUpdated?.(value);
const slider = this._options.getSlider();
if (slider) {
slider.value = value;
}
}
private _showTooltip(): void {
const slider = this._options.getSlider() as any;
slider?.showTooltip?.();
}
private _hideTooltip(): void {
const slider = this._options.getSlider() as any;
slider?.hideTooltip?.();
}
}

View File

@@ -423,12 +423,17 @@ export const formatMediaTime = (seconds: number | undefined): string => {
return "";
}
let secondsString = new Date(seconds * 1000).toISOString();
secondsString =
seconds > 3600
? secondsString.substring(11, 16)
: secondsString.substring(14, 19);
return secondsString.replace(/^0+/, "").padStart(4, "0");
const totalSeconds = Math.max(0, Math.floor(seconds));
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const secs = totalSeconds % 60;
const pad = (value: number) => value.toString().padStart(2, "0");
if (hours > 0) {
return `${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
}
return `${pad(minutes)}:${pad(secs)}`;
};
export const cleanupMediaTitle = (title?: string): string | undefined => {

View File

@@ -14,9 +14,14 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { formatDurationDigital } from "../../../common/datetime/format_duration";
import { stateActive } from "../../../common/entity/state_active";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { debounce } from "../../../common/util/debounce";
import {
startMediaProgressInterval,
stopMediaProgressInterval,
} from "../../../common/util/media-progress";
import { VolumeSliderController } from "../../../common/util/volume-slider";
import "../../../components/chips/ha-assist-chip";
import "../../../components/ha-button";
import "../../../components/ha-icon-button";
@@ -38,6 +43,7 @@ import {
cleanupMediaTitle,
computeMediaControls,
computeMediaDescription,
formatMediaTime,
handleMediaControlClick,
MediaPlayerEntityFeature,
mediaPlayerPlayMedia,
@@ -54,6 +60,34 @@ class MoreInfoMediaPlayer extends LitElement {
@query("#position-slider")
private _positionSlider?: HaSlider;
@query(".volume-slider")
private _volumeSlider?: HaSlider;
private _progressInterval?: number;
private _volumeStep = 2;
private _debouncedVolumeSet = debounce((value: number) => {
this._setVolume(value);
}, 100);
private _volumeController = new VolumeSliderController({
getSlider: () => this._volumeSlider,
step: this._volumeStep,
onSetVolume: (value) => this._setVolume(value),
onSetVolumeDebounced: (value) => this._debouncedVolumeSet(value),
});
public connectedCallback(): void {
super.connectedCallback();
this._syncProgressInterval();
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this._clearProgressInterval();
}
protected firstUpdated(_changedProperties: PropertyValues) {
if (this._positionSlider) {
this._positionSlider.valueFormatter = (value: number) =>
@@ -62,14 +96,7 @@ class MoreInfoMediaPlayer extends LitElement {
}
private _formatDuration(duration: number) {
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
const seconds = Math.floor(duration % 60);
return formatDurationDigital(this.hass.locale, {
hours,
minutes,
seconds,
})!;
return formatMediaTime(duration);
}
protected _renderVolumeControl() {
@@ -139,13 +166,25 @@ class MoreInfoMediaPlayer extends LitElement {
${!supportsMute
? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>`
: nothing}
<ha-slider
labeled
id="input"
.value=${Number(this.stateObj.attributes.volume_level) *
100}
@change=${this._selectedValueChanged}
></ha-slider>
<div
class="volume-slider-container"
@touchstart=${this._volumeController.handleTouchStart}
@touchmove=${this._volumeController.handleTouchMove}
@touchend=${this._volumeController.handleTouchEnd}
@touchcancel=${this._volumeController.handleTouchCancel}
@wheel=${this._volumeController.handleWheel}
>
<ha-slider
class="volume-slider"
labeled
id="input"
.value=${Number(this.stateObj.attributes.volume_level) *
100}
.step=${this._volumeStep}
@input=${this._volumeController.handleInput}
@change=${this._volumeController.handleChange}
></ha-slider>
</div>
`
: nothing}
</div>
@@ -261,17 +300,17 @@ class MoreInfoMediaPlayer extends LitElement {
const stateObj = this.stateObj;
const controls = computeMediaControls(stateObj, true);
const coverUrl =
const coverUrlRaw =
stateObj.attributes.entity_picture_local ||
stateObj.attributes.entity_picture ||
"";
const coverUrl = coverUrlRaw ? this.hass.hassUrl(coverUrlRaw) : "";
const playerObj = new HassMediaPlayerEntity(this.hass, this.stateObj);
const position = Math.max(Math.floor(playerObj.currentProgress || 0), 0);
const duration = Math.max(stateObj.attributes.media_duration || 0, 0);
const remaining = Math.max(duration - position, 0);
const remainingFormatted = this._formatDuration(remaining);
const positionFormatted = this._formatDuration(position);
const durationFormatted = this._formatDuration(duration);
const primaryTitle = cleanupMediaTitle(stateObj.attributes.media_title);
const secondaryTitle = computeMediaDescription(stateObj);
const turnOn = controls?.find((c) => c.action === "turn_on");
@@ -331,8 +370,12 @@ class MoreInfoMediaPlayer extends LitElement {
?disabled=${!stateActive(stateObj) ||
!supportsFeature(stateObj, MediaPlayerEntityFeature.SEEK)}
>
<span slot="reference">${positionFormatted}</span>
<span slot="reference">${remainingFormatted}</span>
<span class="position-time" slot="reference"
>${positionFormatted}</span
>
<span class="position-time" slot="reference"
>${durationFormatted}</span
>
</ha-slider>
</div>
`
@@ -531,6 +574,16 @@ class MoreInfoMediaPlayer extends LitElement {
margin-left: var(--ha-space-2);
}
.volume-slider-container {
width: 100%;
}
@media (pointer: coarse) {
.volume-slider {
pointer-events: none;
}
}
.volume ha-svg-icon {
padding: var(--ha-space-1);
height: 16px;
@@ -568,6 +621,10 @@ class MoreInfoMediaPlayer extends LitElement {
color: var(--secondary-text-color);
}
.position-time {
margin-top: var(--ha-space-2);
}
.media-info-row {
display: flex;
flex-direction: column;
@@ -622,6 +679,39 @@ class MoreInfoMediaPlayer extends LitElement {
);
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("stateObj")) {
this._syncProgressInterval();
}
}
private _syncProgressInterval(): void {
if (this._shouldUpdateProgress()) {
this._progressInterval = startMediaProgressInterval(
this._progressInterval,
() => this.requestUpdate()
);
return;
}
this._clearProgressInterval();
}
private _clearProgressInterval(): void {
this._progressInterval = stopMediaProgressInterval(this._progressInterval);
}
private _shouldUpdateProgress(): boolean {
const stateObj = this.stateObj;
return (
!!stateObj &&
stateObj.state === "playing" &&
Number(stateObj.attributes.media_duration) > 0 &&
"media_position" in stateObj.attributes &&
"media_position_updated_at" in stateObj.attributes
);
}
private _toggleMute() {
this.hass!.callService("media_player", "volume_mute", {
entity_id: this.stateObj!.entity_id,
@@ -629,10 +719,10 @@ class MoreInfoMediaPlayer extends LitElement {
});
}
private _selectedValueChanged(e: Event): void {
private _setVolume(value: number) {
this.hass!.callService("media_player", "volume_set", {
entity_id: this.stateObj!.entity_id,
volume_level: (e.target as any).value / 100,
volume_level: value / 100,
});
}

View File

@@ -1,5 +1,3 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import type { LinearProgress } from "@material/mwc-linear-progress/mwc-linear-progress";
import {
mdiChevronDown,
mdiMonitor,
@@ -20,11 +18,16 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { supportsFeature } from "../../common/entity/supports-feature";
import { debounce } from "../../common/util/debounce";
import {
startMediaProgressInterval,
stopMediaProgressInterval,
} from "../../common/util/media-progress";
import { VolumeSliderController } from "../../common/util/volume-slider";
import "../../components/ha-button";
import "../../components/ha-button-menu";
import "../../components/ha-domain-icon";
import "../../components/ha-dropdown";
import "../../components/ha-dropdown-item";
import "../../components/ha-icon-button";
import "../../components/ha-list-item";
import "../../components/ha-slider";
import "../../components/ha-spinner";
import "../../components/ha-state-icon";
@@ -50,6 +53,7 @@ import type { ResolvedMediaSource } from "../../data/media_source";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types";
import type { HaSlider } from "../../components/ha-slider";
import "../lovelace/components/hui-marquee";
import {
BrowserMediaPlayer,
@@ -70,20 +74,40 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
@property({ type: Boolean, reflect: true }) public narrow = false;
@query("mwc-linear-progress") private _progressBar?: LinearProgress;
@query(".progress-slider") private _progressBar?: HaSlider;
@query("#CurrentProgress") private _currentProgress?: HTMLElement;
@query(".volume-slider") private _volumeSlider?: HaSlider;
@state() private _marqueeActive = false;
@state() private _newMediaExpected = false;
@state() private _browserPlayer?: BrowserMediaPlayer;
private _volumeValue = 0;
private _progressInterval?: number;
private _browserPlayerVolume = 0.8;
private _volumeStep = 2;
private _debouncedVolumeSet = debounce((value: number) => {
this._setVolume(value);
}, 100);
private _volumeController = new VolumeSliderController({
getSlider: () => this._volumeSlider,
step: this._volumeStep,
onSetVolume: (value) => this._setVolume(value),
onSetVolumeDebounced: (value) => this._debouncedVolumeSet(value),
onValueUpdated: (value) => {
this._volumeValue = value;
},
});
public connectedCallback(): void {
super.connectedCallback();
@@ -94,23 +118,20 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
}
if (
!this._progressInterval &&
this._showProgressBar &&
stateObj.state === "playing"
stateObj.state === "playing" &&
!this._progressInterval
) {
this._progressInterval = window.setInterval(
() => this._updateProgressBar(),
1000
this._progressInterval = startMediaProgressInterval(
this._progressInterval,
() => this._updateProgressBar()
);
}
}
public disconnectedCallback(): void {
super.disconnectedCallback();
if (this._progressInterval) {
clearInterval(this._progressInterval);
this._progressInterval = undefined;
}
this._progressInterval = stopMediaProgressInterval(this._progressInterval);
this._tearDownBrowserPlayer();
}
@@ -174,7 +195,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
const stateObj = this._stateObj;
if (!stateObj) {
return this._renderChoosePlayer(stateObj);
return this._renderChoosePlayer(stateObj, this._volumeValue);
}
const controls: ControlButton[] | undefined = !this.narrow
@@ -214,7 +235,6 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
const mediaArt =
stateObj.attributes.entity_picture_local ||
stateObj.attributes.entity_picture;
return html`
<div
class=${classMap({
@@ -271,21 +291,55 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
${stateObj.attributes.media_duration === Infinity
? nothing
: this.narrow
? html`<mwc-linear-progress></mwc-linear-progress>`
? html`<ha-slider
class="progress-slider"
min="0"
max=${stateObj.attributes.media_duration || 0}
step="1"
.value=${getCurrentProgress(stateObj)}
.withTooltip=${false}
size="small"
aria-label=${this.hass.localize(
"ui.card.media_player.track_position"
)}
?disabled=${isBrowser ||
!supportsFeature(stateObj, MediaPlayerEntityFeature.SEEK)}
@change=${this._handleMediaSeekChanged}
></ha-slider>`
: html`
<div class="progress">
<div id="CurrentProgress"></div>
<mwc-linear-progress wide></mwc-linear-progress>
<ha-slider
class="progress-slider"
min="0"
max=${stateObj.attributes.media_duration || 0}
step="1"
.value=${getCurrentProgress(stateObj)}
.withTooltip=${false}
size="small"
aria-label=${this.hass.localize(
"ui.card.media_player.track_position"
)}
?disabled=${isBrowser ||
!supportsFeature(
stateObj,
MediaPlayerEntityFeature.SEEK
)}
@change=${this._handleMediaSeekChanged}
></ha-slider>
<div>${mediaDuration}</div>
</div>
`}
`}
</div>
${this._renderChoosePlayer(stateObj)}
${this._renderChoosePlayer(stateObj, this._volumeValue)}
`;
}
private _renderChoosePlayer(stateObj: MediaPlayerEntity | undefined) {
private _renderChoosePlayer(
stateObj: MediaPlayerEntity | undefined,
volumeValue: number
) {
const isBrowser = this.entityId === BROWSER_PLAYER;
return html`
<div class="choose-player ${isBrowser ? "browser" : ""}">
@@ -294,26 +348,42 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
stateObj &&
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET)
? html`
<ha-button-menu y="0" x="76">
<ha-dropdown class="volume-menu" placement="top" .distance=${8}>
<ha-icon-button
slot="trigger"
.path=${mdiVolumeHigh}
></ha-icon-button>
<ha-slider
labeled
min="0"
max="100"
step="1"
.value=${stateObj.attributes.volume_level! * 100}
@change=${this._handleVolumeChange}
<div
class="volume-slider-container"
@touchstart=${this._volumeController.handleTouchStart}
@touchmove=${this._volumeController.handleTouchMove}
@touchend=${this._volumeController.handleTouchEnd}
@touchcancel=${this._volumeController.handleTouchCancel}
@wheel=${this._volumeController.handleWheel}
>
</ha-slider>
</ha-button-menu>
<ha-slider
class="volume-slider"
labeled
min="0"
max="100"
.step=${this._volumeStep}
.value=${volumeValue}
@input=${this._volumeController.handleInput}
@change=${this._volumeController.handleChange}
>
</ha-slider>
</div>
</ha-dropdown>
`
: ""
}
<ha-button-menu>
<ha-dropdown
class="player-menu"
placement="top-end"
.distance=${8}
@wa-select=${this._handlePlayerSelect}
>
${
this.narrow
? html`
@@ -342,26 +412,24 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
</ha-button>
`
}
<ha-list-item
.player=${BROWSER_PLAYER}
?selected=${isBrowser}
@click=${this._selectPlayer}
<ha-dropdown-item
class=${isBrowser ? "selected" : ""}
.value=${BROWSER_PLAYER}
>
${this.hass.localize("ui.components.media-browser.web-browser")}
</ha-list-item>
</ha-dropdown-item>
${this._mediaPlayerEntities.map(
(source) => html`
<ha-list-item
?selected=${source.entity_id === this.entityId}
<ha-dropdown-item
class=${source.entity_id === this.entityId ? "selected" : ""}
.disabled=${source.state === UNAVAILABLE}
.player=${source.entity_id}
@click=${this._selectPlayer}
.value=${source.entity_id}
>
${computeStateName(source)}
</ha-list-item>
</ha-dropdown-item>
`
)}
</ha-button-menu>
</ha-dropdown>
</div>
</div>
@@ -401,6 +469,9 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
) {
this._newMediaExpected = false;
}
if (changedProps.has("hass")) {
this._updateVolumeValueFromState(this._stateObj);
}
}
protected updated(changedProps: PropertyValues) {
@@ -419,23 +490,25 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
const stateObj = this._stateObj;
this._updateProgressBar();
if (this.entityId === BROWSER_PLAYER) {
this._updateVolumeValueFromState(stateObj);
}
if (
!this._progressInterval &&
this._showProgressBar &&
stateObj?.state === "playing"
) {
this._progressInterval = window.setInterval(
() => this._updateProgressBar(),
1000
this._updateProgressBar();
this._syncVolumeSlider();
if (this._showProgressBar && stateObj?.state === "playing") {
this._progressInterval = startMediaProgressInterval(
this._progressInterval,
() => this._updateProgressBar()
);
} else if (
this._progressInterval &&
(!this._showProgressBar || stateObj?.state !== "playing")
) {
clearInterval(this._progressInterval);
this._progressInterval = undefined;
this._progressInterval = stopMediaProgressInterval(
this._progressInterval
);
}
}
@@ -489,25 +562,45 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
private _updateProgressBar(): void {
const stateObj = this._stateObj;
if (!this._progressBar || !this._currentProgress || !stateObj) {
if (!this._progressBar || !stateObj) {
return;
}
if (!stateObj.attributes.media_duration) {
this._progressBar.progress = 0;
this._currentProgress.innerHTML = "";
this._progressBar.value = 0;
if (this._currentProgress) {
this._currentProgress.innerHTML = "";
}
return;
}
const currentProgress = getCurrentProgress(stateObj);
this._progressBar.progress =
currentProgress / stateObj.attributes.media_duration;
this._progressBar.max = stateObj.attributes.media_duration;
this._progressBar.value = currentProgress;
if (this._currentProgress) {
this._currentProgress.innerHTML = formatMediaTime(currentProgress);
}
}
private _updateVolumeValueFromState(stateObj?: MediaPlayerEntity): void {
if (!stateObj) {
return;
}
const volumeLevel = stateObj.attributes.volume_level;
if (typeof volumeLevel !== "number" || !Number.isFinite(volumeLevel)) {
return;
}
this._volumeValue = Math.round(volumeLevel * 100);
}
private _syncVolumeSlider(): void {
if (!this._volumeSlider || this._volumeController.isInteracting) {
return;
}
this._volumeSlider.value = this._volumeValue;
}
private _handleControlClick(e: MouseEvent): void {
const action = (e.currentTarget! as HTMLElement).getAttribute("action")!;
@@ -526,6 +619,18 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
}
}
private _handleMediaSeekChanged(e: Event): void {
if (this.entityId === BROWSER_PLAYER || !this._stateObj) {
return;
}
const newValue = (e.target as HaSlider).value;
this.hass.callService("media_player", "media_seek", {
entity_id: this._stateObj.entity_id,
seek_position: newValue,
});
}
private _marqueeMouseOver(): void {
if (!this._marqueeActive) {
this._marqueeActive = true;
@@ -538,20 +643,19 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
}
}
private _selectPlayer(ev: CustomEvent): void {
const entityId = (ev.currentTarget as any).player;
private _handlePlayerSelect(ev: CustomEvent): void {
const entityId = (ev.detail.item as any).value;
fireEvent(this, "player-picked", { entityId });
}
private async _handleVolumeChange(ev) {
ev.stopPropagation();
const value = Number(ev.target.value) / 100;
private _setVolume(value: number) {
const volume = value / 100;
if (this._browserPlayer) {
this._browserPlayerVolume = value;
this._browserPlayer.setVolume(value);
} else {
await setMediaPlayerVolume(this.hass, this.entityId, value);
this._browserPlayerVolume = volume;
this._browserPlayer.setVolume(volume);
return;
}
setMediaPlayerVolume(this.hass, this.entityId, volume);
}
static styles = css`
@@ -570,10 +674,11 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
margin-left: var(--safe-area-inset-left);
}
mwc-linear-progress {
ha-slider {
width: 100%;
padding: 0 4px;
--mdc-theme-primary: var(--secondary-text-color);
min-width: 100%;
--ha-slider-thumb-color: var(--primary-color);
--ha-slider-indicator-color: var(--primary-color);
}
ha-button-menu ha-button[slot="trigger"] {
@@ -611,6 +716,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
justify-content: flex-end;
align-items: center;
padding: 16px;
gap: var(--ha-space-2);
}
.controls {
@@ -633,10 +739,35 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
align-items: center;
}
mwc-linear-progress[wide] {
.progress > div:first-child {
margin-right: var(--ha-space-2);
}
.progress > div:last-child {
margin-left: var(--ha-space-2);
}
.progress ha-slider {
margin: 0 4px;
}
ha-dropdown.volume-menu::part(menu) {
width: 220px;
max-width: 220px;
overflow: visible;
padding: 15px 15px;
}
.volume-slider-container {
width: 100%;
}
@media (pointer: coarse) {
.volume-slider {
pointer-events: none;
}
}
.media-info {
text-overflow: ellipsis;
white-space: nowrap;
@@ -700,14 +831,14 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
justify-content: flex-end;
}
:host([narrow]) mwc-linear-progress {
padding: 0;
:host([narrow]) ha-slider {
position: absolute;
top: -4px;
top: -6px;
left: 0;
right: 0;
}
ha-list-item[selected] {
ha-dropdown-item.selected {
font-weight: var(--ha-font-weight-bold);
}
`;