Media browser (#6672)
* Add media browser stub * Updates from first night * Visual updates * First pr push? * Updates * Add to dialog Havent tested it idk where to put it * comments - Add overflow menu * change to flex end * lint * Refresh the previous item * simplify child render logic * Add show media browser dialog func (thanks bram) * Add to more info dialog. Not perfect. Visual bugs * Change play/picked event to callback * Don't use data table * Move play button * Fix dialog getting too wide * Style tweaks * tweaks * Fix padding mobile * Update ha-media-player-browse.ts * Remove Color on folder icon * Leave dialog open on play * Move more info icon * Remove unneeded files Co-authored-by: Paulus Schoutsen <balloob@gmail.com> Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
226b2a73af
commit
5fd8b5c5b9
|
@ -3,19 +3,21 @@ import {
|
|||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
eventOptions,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
eventOptions,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { scroll } from "lit-virtualizer";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../common/search/search-input";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
|
@ -24,8 +26,6 @@ import "../ha-checkbox";
|
|||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-icon";
|
||||
import { filterData, sortData } from "./sort-filter";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
} from "../../data/media-player";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { createCloseHeading } from "../ha-dialog";
|
||||
import "./ha-media-player-browse";
|
||||
import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog";
|
||||
|
||||
@customElement("dialog-media-player-browse")
|
||||
class DialogMediaPlayerBrowse extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _entityId!: string;
|
||||
|
||||
@internalProperty() private _mediaContentId?: string;
|
||||
|
||||
@internalProperty() private _mediaContentType?: string;
|
||||
|
||||
@internalProperty() private _action?: MediaPlayerBrowseAction;
|
||||
|
||||
@internalProperty() private _params?: MediaPlayerBrowseDialogParams;
|
||||
|
||||
public async showDialog(
|
||||
params: MediaPlayerBrowseDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._entityId = this._params.entityId;
|
||||
this._mediaContentId = this._params.mediaContentId;
|
||||
this._mediaContentType = this._params.mediaContentType;
|
||||
this._action = this._params.action || "play";
|
||||
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.components.media-browser.media-player-browser")
|
||||
)}
|
||||
@closed=${this._closeDialog}
|
||||
>
|
||||
<ha-media-player-browse
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.action=${this._action!}
|
||||
.mediaContentId=${this._mediaContentId}
|
||||
.mediaContentType=${this._mediaContentType}
|
||||
@media-picked=${this._mediaPicked}
|
||||
></ha-media-player-browse>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _closeDialog() {
|
||||
this._params = undefined;
|
||||
}
|
||||
|
||||
private _mediaPicked(ev: HASSDomEvent<MediaPickedEvent>): void {
|
||||
this._params!.mediaPickedCallback(ev.detail);
|
||||
if (this._action !== "play") {
|
||||
this._closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 8;
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
}
|
||||
ha-media-player-browse {
|
||||
width: 700px;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-media-player-browse": DialogMediaPlayerBrowse;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,581 @@
|
|||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-fab/mwc-fab";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiArrowLeft, mdiFolder, mdiPlay, mdiPlus } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { browseMediaPlayer, MediaPickedEvent } from "../../data/media-player";
|
||||
import type { MediaPlayerItem } from "../../data/media-player";
|
||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-picker";
|
||||
import "../ha-button-menu";
|
||||
import "../ha-card";
|
||||
import "../ha-circular-progress";
|
||||
import "../ha-paper-dropdown-menu";
|
||||
import "../ha-svg-icon";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"media-picked": MediaPickedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-media-player-browse")
|
||||
export class HaMediaPlayerBrowse extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public entityId!: string;
|
||||
|
||||
@property() public mediaContentId?: string;
|
||||
|
||||
@property() public mediaContentType?: string;
|
||||
|
||||
@property() public action: "pick" | "play" = "play";
|
||||
|
||||
@property({ type: Boolean, attribute: "narrow", reflect: true })
|
||||
private _narrow = false;
|
||||
|
||||
@internalProperty() private _loading = false;
|
||||
|
||||
@internalProperty() private _mediaPlayerItems: MediaPlayerItem[] = [];
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.updateComplete.then(() => this._attachObserver());
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._mediaPlayerItems.length) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (this._loading) {
|
||||
return html`<ha-circular-progress active></ha-circular-progress>`;
|
||||
}
|
||||
|
||||
const mostRecentItem = this._mediaPlayerItems[
|
||||
this._mediaPlayerItems.length - 1
|
||||
];
|
||||
const previousItem =
|
||||
this._mediaPlayerItems.length > 1
|
||||
? this._mediaPlayerItems[this._mediaPlayerItems.length - 2]
|
||||
: undefined;
|
||||
|
||||
const hasExpandableChildren:
|
||||
| MediaPlayerItem
|
||||
| undefined = this._hasExpandableChildren(mostRecentItem.children);
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
${mostRecentItem.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="img"
|
||||
style="background-image: url(${mostRecentItem.thumbnail})"
|
||||
>
|
||||
${this._narrow && mostRecentItem?.can_play
|
||||
? html`
|
||||
<mwc-fab
|
||||
mini
|
||||
.item=${mostRecentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
</mwc-fab>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
<div class="header-info">
|
||||
<div class="breadcrumb-overflow">
|
||||
<div class="breadcrumb">
|
||||
${previousItem
|
||||
? html`
|
||||
<div
|
||||
class="previous-title"
|
||||
.previous=${true}
|
||||
.item=${previousItem}
|
||||
@click=${this._navigate}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
|
||||
${previousItem.title}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<h1 class="title">${mostRecentItem.title}</h1>
|
||||
<h2 class="subtitle">
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.content-type.${mostRecentItem.media_content_type}`
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
${mostRecentItem?.can_play &&
|
||||
(!this._narrow || (this._narrow && !mostRecentItem.thumbnail))
|
||||
? html`
|
||||
<div class="actions">
|
||||
<mwc-button
|
||||
raised
|
||||
.item=${mostRecentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}`
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
${mostRecentItem.children?.length
|
||||
? hasExpandableChildren
|
||||
? html`
|
||||
<div class="children">
|
||||
${mostRecentItem.children?.length
|
||||
? html`
|
||||
${mostRecentItem.children.map(
|
||||
(child) => html`
|
||||
<div
|
||||
class="child"
|
||||
.item=${child}
|
||||
@click=${this._navigate}
|
||||
>
|
||||
<div class="ha-card-parent">
|
||||
<ha-card
|
||||
style="background-image: url(${child.thumbnail})"
|
||||
>
|
||||
${child.can_expand && !child.thumbnail
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${mdiFolder}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
${child.can_play
|
||||
? html`
|
||||
<mwc-icon-button class="play">
|
||||
<ha-svg-icon
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
@click=${this._actionClicked}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="title">${child.title}</div>
|
||||
<div class="type">
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.content-type.${child.media_content_type}`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<mwc-list>
|
||||
${mostRecentItem.children.map(
|
||||
(child) => html`<mwc-list-item
|
||||
@click=${this._actionClicked}
|
||||
.item=${child}
|
||||
graphic="icon"
|
||||
>
|
||||
<span>${child.title}</span>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||||
></ha-svg-icon
|
||||
></mwc-list-item>
|
||||
<li divider role="separator"></li>`
|
||||
)}
|
||||
</mwc-list>
|
||||
`
|
||||
: this.hass.localize("ui.components.media-browser.no_items")}
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._measureCard();
|
||||
this._attachObserver();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (
|
||||
!changedProps.has("entityId") &&
|
||||
!changedProps.has("mediaContentId") &&
|
||||
!changedProps.has("mediaContentType") &&
|
||||
!changedProps.has("action")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._fetchData(this.mediaContentId, this.mediaContentType).then(
|
||||
(itemData) => {
|
||||
this._mediaPlayerItems = [itemData];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _actionClicked(ev: MouseEvent): void {
|
||||
ev.stopPropagation();
|
||||
const item = (ev.currentTarget as any).item;
|
||||
|
||||
this._runAction(item);
|
||||
}
|
||||
|
||||
private _runAction(item: MediaPlayerItem): void {
|
||||
fireEvent(this, "media-picked", {
|
||||
media_content_id: item.media_content_id,
|
||||
media_content_type: item.media_content_type,
|
||||
});
|
||||
}
|
||||
|
||||
private async _navigate(ev: MouseEvent): Promise<void> {
|
||||
const target = ev.currentTarget as any;
|
||||
let item: MediaPlayerItem | undefined;
|
||||
|
||||
if (target.previous) {
|
||||
this._mediaPlayerItems!.pop();
|
||||
item = this._mediaPlayerItems!.pop();
|
||||
}
|
||||
|
||||
item = target.item;
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemData = await this._fetchData(
|
||||
item.media_content_id,
|
||||
item.media_content_type
|
||||
);
|
||||
this._mediaPlayerItems = [...this._mediaPlayerItems, itemData];
|
||||
}
|
||||
|
||||
private async _fetchData(
|
||||
mediaContentId?: string,
|
||||
mediaContentType?: string
|
||||
): Promise<MediaPlayerItem> {
|
||||
const itemData = await browseMediaPlayer(
|
||||
this.hass,
|
||||
this.entityId,
|
||||
!mediaContentId ? undefined : mediaContentId,
|
||||
mediaContentType
|
||||
);
|
||||
|
||||
return itemData;
|
||||
}
|
||||
|
||||
private _measureCard(): void {
|
||||
this._narrow = this.offsetWidth < 500;
|
||||
}
|
||||
|
||||
private async _attachObserver(): Promise<void> {
|
||||
if (!this._resizeObserver) {
|
||||
await installResizeObserver();
|
||||
this._resizeObserver = new ResizeObserver(
|
||||
debounce(() => this._measureCard(), 250, false)
|
||||
);
|
||||
}
|
||||
|
||||
this._resizeObserver.observe(this);
|
||||
}
|
||||
|
||||
private _hasExpandableChildren = memoizeOne((children) =>
|
||||
children.find((item: MediaPlayerItem) => item.can_expand)
|
||||
);
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.breadcrumb-overflow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-grow: 1;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-content .img {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
margin-right: 16px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-self: stretch;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-info .actions {
|
||||
padding-top: 24px;
|
||||
--mdc-theme-primary: var(--primary-color);
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.breadcrumb .title {
|
||||
font-size: 48px;
|
||||
line-height: 1.2;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.breadcrumb .previous-title {
|
||||
font-size: 14px;
|
||||
padding-bottom: 8px;
|
||||
color: var(--secondary-text-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
--mdc-icon-size: 14px;
|
||||
}
|
||||
|
||||
.breadcrumb .subtitle {
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.divider {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.divider::before {
|
||||
height: 1px;
|
||||
display: block;
|
||||
background-color: var(--divider-color);
|
||||
content: " ";
|
||||
}
|
||||
|
||||
/* ============= CHILDREN ============= */
|
||||
|
||||
mwc-list {
|
||||
--mdc-list-vertical-padding: 0;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
mwc-list li:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.children {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
auto-fit,
|
||||
minmax(var(--media-browse-item-size, 175px), 0.33fr)
|
||||
);
|
||||
grid-gap: 16px;
|
||||
margin: 8px 0px;
|
||||
}
|
||||
|
||||
.child {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ha-card-parent {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
width: 100%;
|
||||
padding-bottom: 100%;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.child .folder,
|
||||
.child .play {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.child .folder {
|
||||
color: var(--sidebar-icon-color);
|
||||
top: calc(50% - (var(--mdc-icon-size) / 2));
|
||||
left: calc(50% - (var(--mdc-icon-size) / 2));
|
||||
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
||||
}
|
||||
|
||||
.child .play {
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
transition: all 0.5s;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.child .play:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
ha-card:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.child .title {
|
||||
font-size: 16px;
|
||||
padding-top: 8px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.child .type {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
/* ============= Narrow ============= */
|
||||
|
||||
:host([narrow]) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:host([narrow]) mwc-list {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
:host([narrow]) .breadcrumb .title {
|
||||
font-size: 38px;
|
||||
}
|
||||
|
||||
:host([narrow]) .breadcrumb-overflow {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
:host([narrow]) .header-content {
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
:host([narrow]) .header-content .img {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
padding-bottom: 100%;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([narrow]) .header-content .img mwc-fab {
|
||||
position: absolute;
|
||||
--mdc-theme-secondary: var(--primary-color);
|
||||
bottom: -20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
:host([narrow]) .header-info,
|
||||
:host([narrow]) .media-source,
|
||||
:host([narrow]) .children {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:host([narrow]) .children {
|
||||
grid-template-columns: 1fr 1fr !important;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-media-player-browse": HaMediaPlayerBrowse;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
} from "../../data/media-player";
|
||||
|
||||
export interface MediaPlayerBrowseDialogParams {
|
||||
action: MediaPlayerBrowseAction;
|
||||
entityId: string;
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void;
|
||||
mediaContentId?: string;
|
||||
mediaContentType?: string;
|
||||
}
|
||||
|
||||
export const showMediaBrowserDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: MediaPlayerBrowseDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-media-player-browse",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dialog-media-player-browse" */ "./dialog-media-player-browse"
|
||||
),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
|
@ -1,4 +1,5 @@
|
|||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export const SUPPORT_PAUSE = 1;
|
||||
export const SUPPORT_SEEK = 2;
|
||||
|
@ -14,8 +15,16 @@ export const SUPPORT_SELECT_SOURCE = 2048;
|
|||
export const SUPPORT_STOP = 4096;
|
||||
export const SUPPORTS_PLAY = 16384;
|
||||
export const SUPPORT_SELECT_SOUND_MODE = 65536;
|
||||
export const SUPPORT_BROWSE_MEDIA = 131072;
|
||||
export const CONTRAST_RATIO = 4.5;
|
||||
|
||||
export type MediaPlayerBrowseAction = "pick" | "play";
|
||||
|
||||
export interface MediaPickedEvent {
|
||||
media_content_id: string;
|
||||
media_content_type: string;
|
||||
}
|
||||
|
||||
export interface MediaPlayerThumbnail {
|
||||
content_type: string;
|
||||
content: string;
|
||||
|
@ -26,6 +35,29 @@ export interface ControlButton {
|
|||
action: string;
|
||||
}
|
||||
|
||||
export interface MediaPlayerItem {
|
||||
title: string;
|
||||
media_content_type: string;
|
||||
media_content_id: string;
|
||||
can_play: boolean;
|
||||
can_expand: boolean;
|
||||
thumbnail?: string;
|
||||
children?: MediaPlayerItem[];
|
||||
}
|
||||
|
||||
export const browseMediaPlayer = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
mediaContentId?: string,
|
||||
mediaContentType?: string
|
||||
): Promise<MediaPlayerItem> =>
|
||||
hass.callWS<MediaPlayerItem>({
|
||||
type: "media_player/browse_media",
|
||||
entity_id: entityId,
|
||||
media_content_id: mediaContentId,
|
||||
media_content_type: mediaContentType,
|
||||
});
|
||||
|
||||
export const getCurrentProgress = (stateObj: HassEntity): number => {
|
||||
let progress = stateObj.attributes.media_position;
|
||||
|
||||
|
|
|
@ -1,43 +1,45 @@
|
|||
import "@material/mwc-button/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
customElement,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import { HomeAssistant, MediaEntity } from "../../../types";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { UNAVAILABLE_STATES, UNAVAILABLE, UNKNOWN } from "../../../data/entity";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import "../../../components/ha-slider";
|
||||
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
|
||||
import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity";
|
||||
import {
|
||||
SUPPORT_TURN_ON,
|
||||
SUPPORT_TURN_OFF,
|
||||
ControlButton,
|
||||
MediaPickedEvent,
|
||||
SUPPORTS_PLAY,
|
||||
SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_STOP,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_PLAY_MEDIA,
|
||||
SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_SELECT_SOUND_MODE,
|
||||
SUPPORT_SELECT_SOURCE,
|
||||
SUPPORT_STOP,
|
||||
SUPPORT_TURN_OFF,
|
||||
SUPPORT_TURN_ON,
|
||||
SUPPORT_VOLUME_BUTTONS,
|
||||
SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_VOLUME_SET,
|
||||
SUPPORT_VOLUME_BUTTONS,
|
||||
SUPPORT_SELECT_SOURCE,
|
||||
SUPPORT_SELECT_SOUND_MODE,
|
||||
SUPPORT_PLAY_MEDIA,
|
||||
ControlButton,
|
||||
} from "../../../data/media-player";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
|
||||
import "../../../components/ha-paper-dropdown-menu";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-slider";
|
||||
import "../../../components/ha-icon";
|
||||
import { HomeAssistant, MediaEntity } from "../../../types";
|
||||
|
||||
@customElement("more-info-media_player")
|
||||
class MoreInfoMediaPlayer extends LitElement {
|
||||
|
@ -60,15 +62,29 @@ class MoreInfoMediaPlayer extends LitElement {
|
|||
? ""
|
||||
: html`
|
||||
<div class="controls">
|
||||
${controls!.map(
|
||||
(control) => html`
|
||||
<ha-icon-button
|
||||
action=${control.action}
|
||||
.icon=${control.icon}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
`
|
||||
)}
|
||||
<div class="basic-controls">
|
||||
${controls!.map(
|
||||
(control) => html`
|
||||
<ha-icon-button
|
||||
action=${control.action}
|
||||
.icon=${control.icon}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
${supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
icon="hass:folder-multiple"
|
||||
.title=${this.hass.localize(
|
||||
"ui.card.media_player.browse_media"
|
||||
)}
|
||||
@click=${this._showBrowseMedia}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`}
|
||||
${(supportsFeature(stateObj, SUPPORT_VOLUME_SET) ||
|
||||
|
@ -196,8 +212,16 @@ class MoreInfoMediaPlayer extends LitElement {
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.basic-controls {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.volume,
|
||||
.controls,
|
||||
.source-input,
|
||||
.sound-input,
|
||||
.tts {
|
||||
|
@ -372,6 +396,26 @@ class MoreInfoMediaPlayer extends LitElement {
|
|||
});
|
||||
ttsInput.value = "";
|
||||
}
|
||||
|
||||
private _showBrowseMedia(): void {
|
||||
showMediaBrowserDialog(this, {
|
||||
action: "play",
|
||||
entityId: this.stateObj!.entity_id,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
|
||||
this._playMedia(
|
||||
pickedMedia.media_content_id,
|
||||
pickedMedia.media_content_type
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private _playMedia(media_content_id: string, media_content_type: string) {
|
||||
this.hass!.callService("media_player", "play_media", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
media_content_id,
|
||||
media_content_type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import "../../../components/ha-icon-button";
|
||||
import "@polymer/paper-progress/paper-progress";
|
||||
import type { PaperProgressElement } from "@polymer/paper-progress/paper-progress";
|
||||
import {
|
||||
|
@ -6,9 +5,9 @@ import {
|
|||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
|
@ -25,12 +24,17 @@ import { supportsFeature } from "../../../common/entity/supports-feature";
|
|||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
|
||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
||||
import {
|
||||
computeMediaDescription,
|
||||
CONTRAST_RATIO,
|
||||
ControlButton,
|
||||
getCurrentProgress,
|
||||
MediaPickedEvent,
|
||||
SUPPORTS_PLAY,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_PREVIOUS_TRACK,
|
||||
|
@ -38,17 +42,16 @@ import {
|
|||
SUPPORT_STOP,
|
||||
SUPPORT_TURN_OFF,
|
||||
SUPPORT_TURN_ON,
|
||||
ControlButton,
|
||||
} from "../../../data/media-player";
|
||||
import type { HomeAssistant, MediaEntity } from "../../../types";
|
||||
import { contrast } from "../common/color/contrast";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-marquee";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { MediaControlCardConfig } from "./types";
|
||||
import { installResizeObserver } from "../common/install-resize-observer";
|
||||
import "../components/hui-marquee";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { MediaControlCardConfig } from "./types";
|
||||
|
||||
function getContrastRatio(
|
||||
rgb1: [number, number, number],
|
||||
|
@ -185,7 +188,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
|||
return { type: "media-control", entity: foundEntities[0] || "" };
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _config?: MediaControlCardConfig;
|
||||
|
||||
|
@ -389,12 +392,27 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
|||
${controls!.map(
|
||||
(control) => html`
|
||||
<ha-icon-button
|
||||
.title=${this.hass.localize(
|
||||
`ui.card.media_player.${control.action}`
|
||||
)}
|
||||
.icon=${control.icon}
|
||||
action=${control.action}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
`
|
||||
)}
|
||||
${supportsFeature(stateObj, SUPPORT_BROWSE_MEDIA)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="browse-media"
|
||||
icon="hass:folder-multiple"
|
||||
.title=${this.hass.localize(
|
||||
"ui.card.media_player.browse_media"
|
||||
)}
|
||||
@click=${this._handleBrowseMedia}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
@ -643,14 +661,31 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
|||
});
|
||||
}
|
||||
|
||||
private _handleBrowseMedia(): void {
|
||||
showMediaBrowserDialog(this, {
|
||||
action: "play",
|
||||
entityId: this._config!.entity,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
|
||||
this._playMedia(
|
||||
pickedMedia.media_content_id,
|
||||
pickedMedia.media_content_type
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private _playMedia(media_content_id: string, media_content_type: string) {
|
||||
this.hass!.callService("media_player", "play_media", {
|
||||
entity_id: this._config!.entity,
|
||||
media_content_id,
|
||||
media_content_type,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleClick(e: MouseEvent): void {
|
||||
this.hass!.callService(
|
||||
"media_player",
|
||||
(e.currentTarget! as HTMLElement).getAttribute("action")!,
|
||||
{
|
||||
entity_id: this._config!.entity,
|
||||
}
|
||||
);
|
||||
const action = (e.currentTarget! as HTMLElement).getAttribute("action")!;
|
||||
this.hass!.callService("media_player", action, {
|
||||
entity_id: this._config!.entity,
|
||||
});
|
||||
}
|
||||
|
||||
private _updateProgressBar(): void {
|
||||
|
@ -831,6 +866,12 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
|||
--mdc-icon-size: 40px;
|
||||
}
|
||||
|
||||
ha-icon-button.browse-media {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
|
||||
.top-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -900,6 +941,10 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
|||
--mdc-icon-size: 36px;
|
||||
}
|
||||
|
||||
.narrow ha-icon-button.browse-media {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
|
||||
.no-progress.player:not(.no-controls) {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
|
|
@ -179,6 +179,13 @@
|
|||
"media_player": {
|
||||
"source": "Source",
|
||||
"sound_mode": "Sound mode",
|
||||
"browse_media": "Browse media",
|
||||
"turn_on": "Turn on",
|
||||
"turn_off": "Turn off",
|
||||
"media_play": "Play",
|
||||
"media_play_pause": "Play/pause",
|
||||
"media_next_track": "Next",
|
||||
"media_previous_track": "Previous",
|
||||
"text_to_speak": "Text to speak"
|
||||
},
|
||||
"persistent_notification": {
|
||||
|
@ -341,6 +348,22 @@
|
|||
"data-table": {
|
||||
"search": "Search",
|
||||
"no-data": "No data"
|
||||
},
|
||||
"media-browser": {
|
||||
"pick": "Pick",
|
||||
"play": "Play",
|
||||
"play-media": "Play Media",
|
||||
"pick-media": "Pick Media",
|
||||
"no_items": "No items",
|
||||
"choose-source": "Choose Source",
|
||||
"media-player-browser": "Media Player Browser",
|
||||
"content-type": {
|
||||
"server": "Server",
|
||||
"library": "Library",
|
||||
"artist": "Artist",
|
||||
"album": "Album",
|
||||
"playlist": "Playlist"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dialogs": {
|
||||
|
|
Loading…
Reference in New Issue