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:
Zack Arnett 2020-08-24 09:31:25 -05:00 committed by GitHub
parent 226b2a73af
commit 5fd8b5c5b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 920 additions and 53 deletions

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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,
});
};

View File

@ -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;

View File

@ -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 {

View File

@ -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;
}

View File

@ -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": {