From 303e065433b720f0de256069beda7bd87dd33e96 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 20 Jan 2022 16:37:30 -0600 Subject: [PATCH] Media Browser Bar (#11369) Co-authored-by: Paulus Schoutsen --- .../media-player/ha-media-player-browse.ts | 59 ++- src/data/media-player.ts | 13 + .../media-browser/ha-bar-media-player.ts | 496 ++++++++++++++++++ .../media-browser/ha-panel-media-browser.ts | 91 +--- .../hui-dialog-select-media-player.ts | 98 ---- .../show-select-media-source-dialog.ts | 18 - src/translations/en.json | 3 +- 7 files changed, 566 insertions(+), 212 deletions(-) create mode 100644 src/panels/media-browser/ha-bar-media-player.ts delete mode 100644 src/panels/media-browser/hui-dialog-select-media-player.ts delete mode 100644 src/panels/media-browser/show-select-media-source-dialog.ts diff --git a/src/components/media-player/ha-media-player-browse.ts b/src/components/media-player/ha-media-player-browse.ts index 7c08bad54c..1d4a9c9cc3 100644 --- a/src/components/media-player/ha-media-player-browse.ts +++ b/src/components/media-player/ha-media-player-browse.ts @@ -413,32 +413,34 @@ export class HaMediaPlayerBrowse extends LitElement { let parentProm: Promise | undefined; // See if we can take loading shortcuts if navigating to parent or child - if ( - // Check if we navigated to a child - oldNavigateIds && - this.navigateIds.length > oldNavigateIds.length && - oldNavigateIds.every((oldVal, idx) => { - const curVal = this.navigateIds[idx]; - return ( - curVal.media_content_id === oldVal.media_content_id && - curVal.media_content_type === oldVal.media_content_type - ); - }) - ) { - parentProm = Promise.resolve(oldCurrentItem!); - } else if ( - // Check if we navigated to a parent - oldNavigateIds && - this.navigateIds.length < oldNavigateIds.length && - this.navigateIds.every((curVal, idx) => { - const oldVal = oldNavigateIds[idx]; - return ( - curVal.media_content_id === oldVal.media_content_id && - curVal.media_content_type === oldVal.media_content_type - ); - }) - ) { - currentProm = Promise.resolve(oldParentItem!); + if (!changedProps.has("entityId")) { + if ( + // Check if we navigated to a child + oldNavigateIds && + this.navigateIds.length > oldNavigateIds.length && + oldNavigateIds.every((oldVal, idx) => { + const curVal = this.navigateIds[idx]; + return ( + curVal.media_content_id === oldVal.media_content_id && + curVal.media_content_type === oldVal.media_content_type + ); + }) + ) { + parentProm = Promise.resolve(oldCurrentItem!); + } else if ( + // Check if we navigated to a parent + oldNavigateIds && + this.navigateIds.length < oldNavigateIds.length && + this.navigateIds.every((curVal, idx) => { + const oldVal = oldNavigateIds[idx]; + return ( + curVal.media_content_id === oldVal.media_content_id && + curVal.media_content_type === oldVal.media_content_type + ); + }) + ) { + currentProm = Promise.resolve(oldParentItem!); + } } // Fetch current if (!currentProm) { @@ -710,7 +712,7 @@ export class HaMediaPlayerBrowse extends LitElement { right: 0; left: 0; z-index: 5; - padding: 20px 24px 10px; + padding: 20px 24px 10px 32px; } .header_button { @@ -809,8 +811,7 @@ export class HaMediaPlayerBrowse extends LitElement { minmax(var(--media-browse-item-size, 175px), 0.1fr) ); grid-gap: 16px; - padding: 0px 24px; - margin: 8px 0px; + padding: 8px; } :host([dialog]) .children { diff --git a/src/data/media-player.ts b/src/data/media-player.ts index ee4245f1b9..cfb9c615d3 100644 --- a/src/data/media-player.ts +++ b/src/data/media-player.ts @@ -320,3 +320,16 @@ export const computeMediaControls = ( return buttons.length > 0 ? buttons : undefined; }; + +export const formatMediaTime = (seconds: number): string => { + if (!seconds) { + 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"); +}; diff --git a/src/panels/media-browser/ha-bar-media-player.ts b/src/panels/media-browser/ha-bar-media-player.ts new file mode 100644 index 0000000000..b8864ed93f --- /dev/null +++ b/src/panels/media-browser/ha-bar-media-player.ts @@ -0,0 +1,496 @@ +import "@material/mwc-button/mwc-button"; +import "@material/mwc-linear-progress/mwc-linear-progress"; +import type { LinearProgress } from "@material/mwc-linear-progress/mwc-linear-progress"; +import "@material/mwc-list/mwc-list-item"; +import { + mdiChevronDown, + mdiMonitor, + mdiPause, + mdiPlay, + mdiPlayPause, + mdiStop, +} from "@mdi/js"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { computeDomain } from "../../common/entity/compute_domain"; +import { computeStateDomain } from "../../common/entity/compute_state_domain"; +import { computeStateName } from "../../common/entity/compute_state_name"; +import { domainIcon } from "../../common/entity/domain_icon"; +import { supportsFeature } from "../../common/entity/supports-feature"; +import { navigate } from "../../common/navigate"; +import "../../components/ha-button-menu"; +import "../../components/ha-icon-button"; +import { UNAVAILABLE_STATES } from "../../data/entity"; +import { + BROWSER_PLAYER, + computeMediaControls, + computeMediaDescription, + formatMediaTime, + getCurrentProgress, + MediaPlayerEntity, + SUPPORT_BROWSE_MEDIA, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_STOP, +} from "../../data/media-player"; +import type { HomeAssistant } from "../../types"; +import "../lovelace/components/hui-marquee"; + +@customElement("ha-bar-media-player") +class BarMediaPlayer extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public entityId!: string; + + @property({ type: Boolean, reflect: true }) + public narrow!: boolean; + + @query("mwc-linear-progress") private _progressBar?: LinearProgress; + + @query("#CurrentProgress") private _currentProgress?: HTMLElement; + + @state() private _marqueeActive = false; + + private _progressInterval?: number; + + public connectedCallback(): void { + super.connectedCallback(); + + const stateObj = this._stateObj; + + if (!stateObj) { + return; + } + + if ( + !this._progressInterval && + this._showProgressBar && + stateObj.state === "playing" + ) { + this._progressInterval = window.setInterval( + () => this._updateProgressBar(), + 1000 + ); + } + } + + public disconnectedCallback(): void { + if (this._progressInterval) { + clearInterval(this._progressInterval); + this._progressInterval = undefined; + } + } + + protected render(): TemplateResult { + const choosePlayerElement = html` +
+ + ${this.narrow + ? html` + + ` + : html` + + + + + `} + ${this.hass.localize( + "ui.components.media-browser.web-browser" + )} + ${this._mediaPlayerEntities.map( + (source) => html` + ${computeStateName(source)} + ` + )} + +
+ `; + + if (!this._stateObj) { + return choosePlayerElement; + } + + const stateObj = this._stateObj; + const controls = !this.narrow + ? computeMediaControls(stateObj) + : (stateObj.state === "playing" && + (supportsFeature(stateObj, SUPPORT_PAUSE) || + supportsFeature(stateObj, SUPPORT_STOP))) || + ((stateObj.state === "paused" || stateObj.state === "idle") && + supportsFeature(stateObj, SUPPORT_PLAY)) || + (stateObj.state === "on" && + (supportsFeature(stateObj, SUPPORT_PLAY) || + supportsFeature(stateObj, SUPPORT_PAUSE))) + ? [ + { + icon: + stateObj.state === "on" + ? mdiPlayPause + : stateObj.state !== "playing" + ? mdiPlay + : supportsFeature(stateObj, SUPPORT_PAUSE) + ? mdiPause + : mdiStop, + action: + stateObj.state !== "playing" + ? "media_play" + : supportsFeature(stateObj, SUPPORT_PAUSE) + ? "media_pause" + : "media_stop", + }, + ] + : [{}]; + const mediaDescription = computeMediaDescription(stateObj); + const mediaDuration = formatMediaTime(stateObj!.attributes.media_duration!); + + return html` +
+ ${this._image + ? html`` + : stateObj.state === "off" || stateObj.state !== "playing" + ? html`
` + : ""} +
+ + + ${stateObj.attributes.media_title ? mediaDescription : ""} + +
+
+
+
+ ${controls!.map( + (control) => html` + + + ` + )} +
+ ${this.narrow + ? html`` + : html` +
+
+ +
${mediaDuration}
+
+ `} +
+ ${choosePlayerElement} + `; + } + + protected updated(changedProps: PropertyValues) { + if (!this.hass || !this._stateObj || !changedProps.has("hass")) { + return; + } + + const stateObj = this._stateObj; + + this._updateProgressBar(); + + if ( + !this._progressInterval && + this._showProgressBar && + stateObj.state === "playing" + ) { + this._progressInterval = window.setInterval( + () => this._updateProgressBar(), + 1000 + ); + } else if ( + this._progressInterval && + (!this._showProgressBar || stateObj.state !== "playing") + ) { + clearInterval(this._progressInterval); + this._progressInterval = undefined; + } + } + + private get _stateObj(): MediaPlayerEntity | undefined { + return this.hass!.states[this.entityId] as MediaPlayerEntity; + } + + private get _showProgressBar() { + if (!this.hass) { + return false; + } + + const stateObj = this._stateObj; + + if (!stateObj) { + return false; + } + + return ( + (stateObj.state === "playing" || stateObj.state === "paused") && + "media_duration" in stateObj.attributes && + "media_position" in stateObj.attributes + ); + } + + private get _image() { + if (!this.hass) { + return undefined; + } + + const stateObj = this._stateObj; + + if (!stateObj) { + return undefined; + } + + return ( + stateObj.attributes.entity_picture_local || + stateObj.attributes.entity_picture + ); + } + + private get _mediaPlayerEntities() { + return Object.values(this.hass!.states).filter((entity) => { + if ( + computeStateDomain(entity) === "media_player" && + supportsFeature(entity, SUPPORT_BROWSE_MEDIA) + ) { + return true; + } + + return false; + }); + } + + private _updateProgressBar(): void { + if (this._progressBar && this._stateObj?.attributes.media_duration) { + const currentProgress = getCurrentProgress(this._stateObj); + this._progressBar.progress = + currentProgress / this._stateObj!.attributes.media_duration; + + if (this._currentProgress) { + this._currentProgress.innerHTML = formatMediaTime(currentProgress); + } + } + } + + private _handleClick(e: MouseEvent): void { + const action = (e.currentTarget! as HTMLElement).getAttribute("action")!; + this.hass!.callService("media_player", action, { + entity_id: this.entityId, + }); + } + + private _marqueeMouseOver(): void { + if (!this._marqueeActive) { + this._marqueeActive = true; + } + } + + private _marqueeMouseLeave(): void { + if (this._marqueeActive) { + this._marqueeActive = false; + } + } + + private _selectPlayer(ev: CustomEvent): void { + const entityId = (ev.currentTarget as any).player; + navigate(`/media-browser/${entityId}`, { replace: true }); + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: flex; + min-height: 100px; + background: var( + --ha-card-background, + var(--card-background-color, white) + ); + border-top: 1px solid var(--divider-color); + } + + mwc-linear-progress { + width: 100%; + padding: 0 4px; + --mdc-theme-primary: var(--secondary-text-color); + } + + mwc-button[slot="trigger"] { + --mdc-theme-primary: var(--primary-text-color); + --mdc-icon-size: 36px; + } + + .info { + flex: 1; + display: flex; + align-items: center; + width: 100%; + margin-right: 16px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .secondary, + .progress { + color: var(--secondary-text-color); + } + + .choose-player { + flex: 1; + display: flex; + justify-content: flex-end; + align-items: center; + padding: 16px; + } + + .controls-progress { + flex: 2; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + } + + .progress { + display: flex; + width: 100%; + align-items: center; + } + + mwc-linear-progress[wide] { + margin: 0 4px; + } + + .media-info { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + padding-left: 16px; + width: 100%; + } + + hui-marquee { + font-size: 1.2em; + margin: 0px 0 4px; + } + + img { + max-height: 100px; + } + + .blank-image { + height: 100px; + width: 100px; + background-color: var(--divider-color); + } + + ha-button-menu mwc-button { + line-height: 1; + } + + :host([narrow]) { + min-height: 80px; + max-height: 80px; + } + + :host([narrow]) .controls-progress { + flex: unset; + min-width: 48px; + } + + :host([narrow]) .controls { + display: flex; + } + + :host([narrow]) .choose-player { + padding-left: 0; + min-width: 48px; + flex: unset; + justify-content: center; + } + + :host([narrow]) .choose-player.browser { + justify-content: flex-end; + width: 100%; + } + + :host([narrow]) img { + max-height: 80px; + } + + :host([narrow]) .blank-image { + height: 80px; + width: 80px; + } + + :host([narrow]) mwc-linear-progress { + padding: 0; + position: absolute; + top: -4px; + left: 0; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-bar-media-player": BarMediaPlayer; + } +} diff --git a/src/panels/media-browser/ha-panel-media-browser.ts b/src/panels/media-browser/ha-panel-media-browser.ts index b0ee756b67..6e160d7429 100644 --- a/src/panels/media-browser/ha-panel-media-browser.ts +++ b/src/panels/media-browser/ha-panel-media-browser.ts @@ -11,22 +11,16 @@ import { import { customElement, property } from "lit/decorators"; import { LocalStorage } from "../../common/decorators/local-storage"; import { HASSDomEvent } from "../../common/dom/fire_event"; -import { computeStateDomain } from "../../common/entity/compute_state_domain"; -import { supportsFeature } from "../../common/entity/supports-feature"; import { navigate } from "../../common/navigate"; import "../../components/ha-menu-button"; import "../../components/media-player/ha-media-player-browse"; import type { MediaPlayerItemId } from "../../components/media-player/ha-media-player-browse"; -import { - BROWSER_PLAYER, - MediaPickedEvent, - SUPPORT_BROWSE_MEDIA, -} from "../../data/media-player"; +import { BROWSER_PLAYER, MediaPickedEvent } from "../../data/media-player"; import "../../layouts/ha-app-layout"; import { haStyle } from "../../resources/styles"; import type { HomeAssistant, Route } from "../../types"; +import "./ha-bar-media-player"; import { showWebBrowserPlayMediaDialog } from "./show-media-player-dialog"; -import { showSelectMediaPlayerDialog } from "./show-select-media-source-dialog"; @customElement("ha-panel-media-browser") class PanelMediaBrowser extends LitElement { @@ -48,17 +42,6 @@ class PanelMediaBrowser extends LitElement { private _entityId = BROWSER_PLAYER; protected render(): TemplateResult { - const stateObj = this._entityId - ? this.hass.states[this._entityId] - : undefined; - - const title = - this._entityId === BROWSER_PLAYER - ? `${this.hass.localize("ui.components.media-browser.web-browser")}` - : stateObj?.attributes.friendly_name - ? `${stateObj?.attributes.friendly_name}` - : undefined; - return html` @@ -73,23 +56,22 @@ class PanelMediaBrowser extends LitElement { "ui.components.media-browser.media-player-browser" )} -
${title || ""}
- - ${this.hass.localize("ui.components.media-browser.choose_player")} -
-
- -
+
+ `; } @@ -129,15 +111,6 @@ class PanelMediaBrowser extends LitElement { ]; } - private _showSelectMediaPlayerDialog(): void { - showSelectMediaPlayerDialog(this, { - mediaSources: this._mediaPlayerEntities, - sourceSelectedCallback: (entityId) => { - navigate(`/media-browser/${entityId}`, { replace: true }); - }, - }); - } - private _mediaBrowsed(ev) { if (ev.detail.back) { history.back(); @@ -179,19 +152,6 @@ class PanelMediaBrowser extends LitElement { }); } - private get _mediaPlayerEntities() { - return Object.values(this.hass!.states).filter((entity) => { - if ( - computeStateDomain(entity) === "media_player" && - supportsFeature(entity, SUPPORT_BROWSE_MEDIA) - ) { - return true; - } - - return false; - }); - } - static get styles(): CSSResultGroup { return [ haStyle, @@ -199,21 +159,20 @@ class PanelMediaBrowser extends LitElement { :host { --mdc-theme-primary: var(--app-header-text-color); } + ha-media-player-browse { - height: calc(100vh - var(--header-height)); + height: calc(100vh - (100px + var(--header-height))); } - :host([narrow]) app-toolbar mwc-button { - width: 65px; + + :host([narrow]) ha-media-player-browse { + height: calc(100vh - (80px + var(--header-height))); } - .heading { - overflow: hidden; - white-space: nowrap; - margin-top: 4px; - } - .heading .secondary-text { - font-size: 14px; - overflow: hidden; - text-overflow: ellipsis; + + ha-bar-media-player { + position: absolute; + bottom: 0; + left: 0; + right: 0; } `, ]; diff --git a/src/panels/media-browser/hui-dialog-select-media-player.ts b/src/panels/media-browser/hui-dialog-select-media-player.ts deleted file mode 100644 index 7f92faa9ac..0000000000 --- a/src/panels/media-browser/hui-dialog-select-media-player.ts +++ /dev/null @@ -1,98 +0,0 @@ -import "@material/mwc-list/mwc-list"; -import "@material/mwc-list/mwc-list-item"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators"; -import { fireEvent } from "../../common/dom/fire_event"; -import { computeStateName } from "../../common/entity/compute_state_name"; -import { stringCompare } from "../../common/string/compare"; -import { createCloseHeading } from "../../components/ha-dialog"; -import { UNAVAILABLE_STATES } from "../../data/entity"; -import { BROWSER_PLAYER } from "../../data/media-player"; -import { haStyleDialog } from "../../resources/styles"; -import type { HomeAssistant } from "../../types"; -import type { SelectMediaPlayerDialogParams } from "./show-select-media-source-dialog"; - -@customElement("hui-dialog-select-media-player") -export class HuiDialogSelectMediaPlayer extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) - private _params?: SelectMediaPlayerDialogParams; - - public showDialog(params: SelectMediaPlayerDialogParams): void { - this._params = params; - } - - public closeDialog() { - this._params = undefined; - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - - protected render(): TemplateResult { - if (!this._params) { - return html``; - } - - return html` - - - ${this.hass.localize( - "ui.components.media-browser.web-browser" - )} - ${this._params.mediaSources - .sort((a, b) => - stringCompare(computeStateName(a), computeStateName(b)) - ) - .map( - (source) => html` - ${computeStateName(source)} - ` - )} - - - `; - } - - private _selectPlayer(ev: CustomEvent): void { - const entityId = (ev.currentTarget as any).player; - this._params!.sourceSelectedCallback(entityId); - this.closeDialog(); - } - - static get styles(): CSSResultGroup { - return [ - haStyleDialog, - css` - ha-dialog { - --dialog-content-padding: 0 24px 20px; - } - mwc-list-item[disabled] { - --mdc-theme-text-primary-on-background: var(--disabled-text-color); - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hui-dialog-select-media-player": HuiDialogSelectMediaPlayer; - } -} diff --git a/src/panels/media-browser/show-select-media-source-dialog.ts b/src/panels/media-browser/show-select-media-source-dialog.ts deleted file mode 100644 index 29dbeb1098..0000000000 --- a/src/panels/media-browser/show-select-media-source-dialog.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { HassEntity } from "home-assistant-js-websocket"; -import { fireEvent } from "../../common/dom/fire_event"; - -export interface SelectMediaPlayerDialogParams { - mediaSources: HassEntity[]; - sourceSelectedCallback: (entityId: string) => void; -} - -export const showSelectMediaPlayerDialog = ( - element: HTMLElement, - selectMediaPlayereDialogParams: SelectMediaPlayerDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "hui-dialog-select-media-player", - dialogImport: () => import("./hui-dialog-select-media-player"), - dialogParams: selectMediaPlayereDialogParams, - }); -}; diff --git a/src/translations/en.json b/src/translations/en.json index d7e11559b5..311d011aad 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -203,7 +203,8 @@ "media_volume_down": "Volume down", "media_volume_mute": "Volume mute", "media_volume_unmute": "Volume unmute", - "text_to_speak": "Text to speak" + "text_to_speak": "Text to speak", + "nothing_playing": "Nothing Playing" }, "persistent_notification": { "dismiss": "Dismiss"