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 <balloob@gmail.com>
This commit is contained in:
Jan-Philipp Benecke
2025-09-24 16:13:37 +02:00
committed by GitHub
parent 99d9c67492
commit 7be2c59295
8 changed files with 800 additions and 228 deletions

View File

@@ -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`
<div
class="marquee-container"
@mouseenter=${this._handleMouseEnter}
@mouseleave=${this._handleMouseLeave}
@touchstart=${this._handleMouseEnter}
@touchend=${this._handleMouseLeave}
>
<span class="marquee-text"><slot></slot></span>
</div>
`;
}
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;
}
}