Media Browser: Use Media Class (#6904)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Zack Barett 2020-09-12 11:59:19 -05:00 committed by GitHub
parent 7e70ba6ab2
commit 8b490c5047
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 313 additions and 165 deletions

View File

@ -2,7 +2,7 @@ import "@material/mwc-button/mwc-button";
import "@material/mwc-fab/mwc-fab"; import "@material/mwc-fab/mwc-fab";
import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiArrowLeft, mdiClose, mdiFolder, mdiPlay, mdiPlus } from "@mdi/js"; import { mdiArrowLeft, mdiClose, mdiPlay, mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import { import {
@ -19,7 +19,6 @@ import {
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import { ifDefined } from "lit-html/directives/if-defined"; import { ifDefined } from "lit-html/directives/if-defined";
import { styleMap } from "lit-html/directives/style-map"; import { styleMap } from "lit-html/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl"; import { computeRTLDirection } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
@ -27,6 +26,7 @@ import {
browseLocalMediaPlayer, browseLocalMediaPlayer,
browseMediaPlayer, browseMediaPlayer,
BROWSER_SOURCE, BROWSER_SOURCE,
MediaClassBrowserSettings,
MediaPickedEvent, MediaPickedEvent,
MediaPlayerBrowseAction, MediaPlayerBrowseAction,
} from "../../data/media-player"; } from "../../data/media-player";
@ -93,34 +93,6 @@ export class HaMediaPlayerBrowse extends LitElement {
this._navigate(item); this._navigate(item);
} }
private _renderError(err: { message: string; code: string }) {
if (err.message === "Media directory does not exist.") {
return html`
<h2>No local media found.</h2>
<p>
It looks like you have not yet created a media directory.
<br />Create a directory with the name <b>"media"</b> in the
configuration directory of Home Assistant
(${this.hass.config.config_dir}). <br />Place your video, audio and
image files in this directory to be able to browse and play them in
the browser or on supported media players.
</p>
<p>
Check the
<a
href="https://www.home-assistant.io/integrations/media_source/#local-media"
target="_blank"
rel="noreferrer"
>documentation</a
>
for more info
</p>
`;
}
return err.message;
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (this._loading) { if (this._loading) {
return html`<ha-circular-progress active></ha-circular-progress>`; return html`<ha-circular-progress active></ha-circular-progress>`;
@ -136,7 +108,7 @@ export class HaMediaPlayerBrowse extends LitElement {
text: this._renderError(this._error), text: this._renderError(this._error),
}); });
} else { } else {
return html`<div class="container error"> return html`<div class="container">
${this._renderError(this._error)} ${this._renderError(this._error)}
</div>`; </div>`;
} }
@ -155,17 +127,12 @@ export class HaMediaPlayerBrowse extends LitElement {
? this._mediaPlayerItems[this._mediaPlayerItems.length - 2] ? this._mediaPlayerItems[this._mediaPlayerItems.length - 2]
: undefined; : undefined;
const hasExpandableChildren: const subtitle = this.hass.localize(
| MediaPlayerItem `ui.components.media-browser.class.${currentItem.media_class}`
| undefined = this._hasExpandableChildren(currentItem.children);
const showImages: boolean | undefined = currentItem.children?.some(
(child) => child.thumbnail && child.thumbnail !== currentItem.thumbnail
);
const mediaType = this.hass.localize(
`ui.components.media-browser.content-type.${currentItem.media_content_type}`
); );
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
const childrenMediaClass =
MediaClassBrowserSettings[currentItem.children_media_class];
return html` return html`
<div <div
@ -174,102 +141,113 @@ export class HaMediaPlayerBrowse extends LitElement {
"no-dialog": !this.dialog, "no-dialog": !this.dialog,
})}" })}"
> >
<div class="header-content"> <div class="header-wrapper">
${currentItem.thumbnail <div class="header-content">
? html` ${currentItem.thumbnail
<div ? html`
class="img" <div
style=${styleMap({ class="img"
backgroundImage: currentItem.thumbnail style=${styleMap({
? `url(${currentItem.thumbnail})` backgroundImage: currentItem.thumbnail
: "none", ? `url(${currentItem.thumbnail})`
})} : "none",
> })}
${this._narrow && currentItem?.can_play >
? html` ${this._narrow && currentItem?.can_play
<mwc-fab ? html`
mini <mwc-fab
.item=${currentItem} mini
@click=${this._actionClicked} .item=${currentItem}
> @click=${this._actionClicked}
<ha-svg-icon >
slot="icon" <ha-svg-icon
.label=${this.hass.localize( slot="icon"
`ui.components.media-browser.${this.action}-media` .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}`
)} )}
.path=${this.action === "play" ? mdiPlay : mdiPlus} </mwc-fab>
></ha-svg-icon> `
${this.hass.localize( : ""}
`ui.components.media-browser.${this.action}` </div>
)} `
</mwc-fab> : html``}
` <div class="header-info">
: ""} <div class="breadcrumb">
</div> ${previousItem
` ? html`
: html``} <div class="previous-title" @click=${this.navigateBack}>
<div class="header-info"> <ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
<div class="breadcrumb"> ${previousItem.title}
${previousItem </div>
`
: ""}
<h1 class="title">${currentItem.title}</h1>
${subtitle
? html`
<h2 class="subtitle">
${subtitle}
</h2>
`
: ""}
</div>
${currentItem.can_play &&
(!currentItem.thumbnail || !this._narrow)
? html` ? html`
<div class="previous-title" @click=${this.navigateBack}> <mwc-button
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon> raised
${previousItem.title} .item=${currentItem}
</div> @click=${this._actionClicked}
` >
: ""} <ha-svg-icon
<h1 class="title">${currentItem.title}</h1> slot="icon"
${mediaType .label=${this.hass.localize(
? html` `ui.components.media-browser.${this.action}-media`
<h2 class="subtitle"> )}
${mediaType} .path=${this.action === "play" ? mdiPlay : mdiPlus}
</h2> ></ha-svg-icon>
${this.hass.localize(
`ui.components.media-browser.${this.action}`
)}
</mwc-button>
` `
: ""} : ""}
</div> </div>
${currentItem.can_play && (!currentItem.thumbnail || !this._narrow)
? html`
<mwc-button
raised
.item=${currentItem}
@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>
${this.dialog
? html`
<mwc-icon-button
aria-label=${this.hass.localize("ui.dialogs.generic.close")}
@click=${this._closeDialogAction}
class="header_button"
dir=${computeRTLDirection(this.hass)}
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
</div> </div>
${this.dialog
? html`
<mwc-icon-button
aria-label=${this.hass.localize("ui.dialogs.generic.close")}
@click=${this._closeDialogAction}
class="header_button"
dir=${computeRTLDirection(this.hass)}
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
</div> </div>
${this._error ${this._error
? html`<div class="container error"> ? html`
${this._renderError(this._error)} <div class="container error">
</div>` ${this._renderError(this._error)}
</div>
`
: currentItem.children?.length : currentItem.children?.length
? hasExpandableChildren ? childrenMediaClass.layout === "grid"
? html` ? html`
<div class="children"> <div
class="children ${classMap({
portrait: childrenMediaClass.thumbnail_ratio === "portrait",
})}"
>
${currentItem.children.map( ${currentItem.children.map(
(child) => html` (child) => html`
<div <div
@ -286,11 +264,16 @@ export class HaMediaPlayerBrowse extends LitElement {
: "none", : "none",
})} })}
> >
${child.can_expand && !child.thumbnail ${!child.thumbnail
? html` ? html`
<ha-svg-icon <ha-svg-icon
class="folder" class="folder"
.path=${mdiFolder} .path=${MediaClassBrowserSettings[
child.media_class === "directory"
? child.children_media_class ||
child.media_class
: child.media_class
].icon}
></ha-svg-icon> ></ha-svg-icon>
` `
: ""} : ""}
@ -298,7 +281,9 @@ export class HaMediaPlayerBrowse extends LitElement {
${child.can_play ${child.can_play
? html` ? html`
<mwc-icon-button <mwc-icon-button
class="play" class="play ${classMap({
can_expand: child.can_expand,
})}"
.item=${child} .item=${child}
.label=${this.hass.localize( .label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media` `ui.components.media-browser.${this.action}-media`
@ -330,7 +315,7 @@ export class HaMediaPlayerBrowse extends LitElement {
${currentItem.children.map( ${currentItem.children.map(
(child) => html` (child) => html`
<mwc-list-item <mwc-list-item
@click=${this._actionClicked} @click=${this._childClicked}
.item=${child} .item=${child}
graphic="avatar" graphic="avatar"
hasMeta hasMeta
@ -339,7 +324,7 @@ export class HaMediaPlayerBrowse extends LitElement {
<div <div
class="graphic" class="graphic"
style=${ifDefined( style=${ifDefined(
showImages && child.thumbnail mediaClass.show_list_images && child.thumbnail
? `background-image: url(${child.thumbnail})` ? `background-image: url(${child.thumbnail})`
: undefined : undefined
)} )}
@ -347,7 +332,8 @@ export class HaMediaPlayerBrowse extends LitElement {
> >
<mwc-icon-button <mwc-icon-button
class="play ${classMap({ class="play ${classMap({
show: !showImages || !child.thumbnail, show:
!mediaClass.show_list_images || !child.thumbnail,
})}" })}"
.item=${child} .item=${child}
.label=${this.hass.localize( .label=${this.hass.localize(
@ -367,9 +353,11 @@ export class HaMediaPlayerBrowse extends LitElement {
)} )}
</mwc-list> </mwc-list>
` `
: html`<div class="container"> : html`
${this.hass.localize("ui.components.media-browser.no_items")} <div class="container">
</div>`} ${this.hass.localize("ui.components.media-browser.no_items")}
</div>
`}
`; `;
} }
@ -504,14 +492,38 @@ export class HaMediaPlayerBrowse extends LitElement {
this._resizeObserver.observe(this); this._resizeObserver.observe(this);
} }
private _hasExpandableChildren = memoizeOne((children?: MediaPlayerItem[]) =>
children?.find((item: MediaPlayerItem) => item.can_expand)
);
private _closeDialogAction(): void { private _closeDialogAction(): void {
fireEvent(this, "close-dialog"); fireEvent(this, "close-dialog");
} }
private _renderError(err: { message: string; code: string }) {
if (err.message === "Media directory does not exist.") {
return html`
<h2>No local media found.</h2>
<p>
It looks like you have not yet created a media directory.
<br />Create a directory with the name <b>"media"</b> in the
configuration directory of Home Assistant
(${this.hass.config.config_dir}). <br />Place your video, audio and
image files in this directory to be able to browse and play them in
the browser or on supported media players.
</p>
<p>
Check the
<a
href="https://www.home-assistant.io/integrations/media_source/#local-media"
target="_blank"
rel="noreferrer"
>documentation</a
>
for more info
</p>
`;
}
return html`<span class="error">err.message</span>`;
}
static get styles(): CSSResultArray { static get styles(): CSSResultArray {
return [ return [
haStyle, haStyle,
@ -529,12 +541,9 @@ export class HaMediaPlayerBrowse extends LitElement {
} }
.header { .header {
display: flex; display: block;
justify-content: space-between; justify-content: space-between;
border-bottom: 1px solid var(--divider-color); border-bottom: 1px solid var(--divider-color);
}
.header {
background-color: var(--card-background-color); background-color: var(--card-background-color);
position: sticky; position: sticky;
position: -webkit-sticky; position: -webkit-sticky;
@ -543,6 +552,10 @@ export class HaMediaPlayerBrowse extends LitElement {
padding: 20px 24px 10px; padding: 20px 24px 10px;
} }
.header-wrapper {
display: flex;
}
.header-content { .header-content {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -570,6 +583,7 @@ export class HaMediaPlayerBrowse extends LitElement {
.header-info mwc-button { .header-info mwc-button {
display: block; display: block;
--mdc-theme-primary: var(--primary-color);
} }
.breadcrumb { .breadcrumb {
@ -655,7 +669,7 @@ export class HaMediaPlayerBrowse extends LitElement {
width: 100%; width: 100%;
} }
ha-card { .children ha-card {
width: 100%; width: 100%;
padding-bottom: 100%; padding-bottom: 100%;
position: relative; position: relative;
@ -663,6 +677,11 @@ export class HaMediaPlayerBrowse extends LitElement {
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
transition: padding-bottom 0.1s ease-out;
}
.portrait.children ha-card {
padding-bottom: 150%;
} }
.child .folder, .child .folder,
@ -678,18 +697,36 @@ export class HaMediaPlayerBrowse extends LitElement {
} }
.child .play { .child .play {
transition: color 0.5s;
border-radius: 50%;
bottom: calc(50% - 35px);
right: calc(50% - 35px);
opacity: 0;
transition: opacity 0.1s ease-out;
}
.child .play:not(.can_expand) {
--mdc-icon-button-size: 70px;
--mdc-icon-size: 48px;
}
.ha-card-parent:hover .play:not(.can_expand) {
opacity: 1;
color: var(--primary-color);
}
.child .play.can_expand {
opacity: 1;
background-color: rgba(var(--rgb-card-background-color), 0.5);
bottom: 4px; bottom: 4px;
right: 4px; right: 4px;
transition: all 0.5s;
background-color: rgba(var(--rgb-card-background-color), 0.5);
border-radius: 50%;
} }
.child .play:hover { .child .play:hover {
color: var(--primary-color); color: var(--primary-color);
} }
ha-card:hover { .ha-card-parent:hover ha-card {
opacity: 0.5; opacity: 0.5;
} }
@ -706,6 +743,7 @@ export class HaMediaPlayerBrowse extends LitElement {
.child .type { .child .type {
font-size: 12px; font-size: 12px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
padding-left: 2px;
} }
mwc-list-item .graphic { mwc-list-item .graphic {

View File

@ -1,5 +1,23 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import {
mdiFolder,
mdiPlaylistMusic,
mdiFileMusic,
mdiAlbum,
mdiMusic,
mdiTelevisionClassic,
mdiMovie,
mdiVideo,
mdiImage,
mdiWeb,
mdiGamepadVariant,
mdiAccountMusic,
mdiPodcast,
mdiApplication,
mdiAccountMusicOutline,
mdiDramaMasks,
} from "@mdi/js";
export const SUPPORT_PAUSE = 1; export const SUPPORT_PAUSE = 1;
export const SUPPORT_SEEK = 2; export const SUPPORT_SEEK = 2;
@ -22,6 +40,66 @@ export type MediaPlayerBrowseAction = "pick" | "play";
export const BROWSER_SOURCE = "browser"; export const BROWSER_SOURCE = "browser";
export type MediaClassBrowserSetting = {
icon: string;
thumbnail_ratio?: string;
layout?: string;
show_list_images?: boolean;
};
export const MediaClassBrowserSettings: {
[type: string]: MediaClassBrowserSetting;
} = {
album: { icon: mdiAlbum, layout: "grid" },
app: { icon: mdiApplication, layout: "grid" },
artist: { icon: mdiAccountMusic, layout: "grid", show_list_images: true },
channel: {
icon: mdiTelevisionClassic,
thumbnail_ratio: "portrait",
layout: "grid",
},
composer: {
icon: mdiAccountMusicOutline,
layout: "grid",
show_list_images: true,
},
contributing_artist: {
icon: mdiAccountMusic,
layout: "grid",
show_list_images: true,
},
directory: { icon: mdiFolder, layout: "grid", show_list_images: true },
episode: {
icon: mdiTelevisionClassic,
layout: "grid",
thumbnail_ratio: "portrait",
},
game: {
icon: mdiGamepadVariant,
layout: "grid",
thumbnail_ratio: "portrait",
},
genre: { icon: mdiDramaMasks, layout: "grid", show_list_images: true },
image: { icon: mdiImage, layout: "grid" },
movie: { icon: mdiMovie, thumbnail_ratio: "portrait", layout: "grid" },
music: { icon: mdiMusic },
playlist: { icon: mdiPlaylistMusic, layout: "grid", show_list_images: true },
podcast: { icon: mdiPodcast, layout: "grid" },
season: {
icon: mdiTelevisionClassic,
layout: "grid",
thumbnail_ratio: "portrait",
},
track: { icon: mdiFileMusic },
tv_show: {
icon: mdiTelevisionClassic,
layout: "grid",
thumbnail_ratio: "portrait",
},
url: { icon: mdiWeb },
video: { icon: mdiVideo, layout: "grid" },
};
export interface MediaPickedEvent { export interface MediaPickedEvent {
item: MediaPlayerItem; item: MediaPlayerItem;
} }
@ -40,6 +118,8 @@ export interface MediaPlayerItem {
title: string; title: string;
media_content_type: string; media_content_type: string;
media_content_id: string; media_content_id: string;
media_class: string;
children_media_class: string;
can_play: boolean; can_play: boolean;
can_expand: boolean; can_expand: boolean;
thumbnail?: string; thumbnail?: string;

View File

@ -1,5 +1,4 @@
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import { mdiPlayNetwork } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import { import {
@ -46,9 +45,9 @@ class PanelMediaBrowser extends LitElement {
const title = const title =
this._entityId === BROWSER_SOURCE this._entityId === BROWSER_SOURCE
? `${this.hass.localize("ui.components.media-browser.web-browser")} - ` ? `${this.hass.localize("ui.components.media-browser.web-browser")}`
: stateObj?.attributes.friendly_name : stateObj?.attributes.friendly_name
? `${stateObj?.attributes.friendly_name} - ` ? `${stateObj?.attributes.friendly_name}`
: undefined; : undefined;
return html` return html`
@ -59,17 +58,17 @@ class PanelMediaBrowser extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
></ha-menu-button> ></ha-menu-button>
<div main-title> <div main-title class="heading">
${title || ""}${this.hass.localize( <div>
"ui.components.media-browser.media-player-browser"
)}
</div>
<mwc-button @click=${this._showSelectMediaPlayerDialog}>
<ha-svg-icon .path=${mdiPlayNetwork}></ha-svg-icon>
${this.hass.localize( ${this.hass.localize(
"ui.components.media-browser.choose_player" "ui.components.media-browser.media-player-browser"
)} )}
</mwc-button> </div>
<div class="secondary">${title || ""}</div>
</div>
<mwc-button @click=${this._showSelectMediaPlayerDialog}>
${this.hass.localize("ui.components.media-browser.choose_player")}
</mwc-button>
</app-toolbar> </app-toolbar>
</app-header> </app-header>
<div class="content"> <div class="content">
@ -134,9 +133,25 @@ class PanelMediaBrowser extends LitElement {
return [ return [
haStyle, haStyle,
css` css`
:host {
--mdc-theme-primary: var(--app-header-text-color);
}
ha-media-player-browse { ha-media-player-browse {
height: calc(100vh - 84px); height: calc(100vh - 84px);
} }
:host([narrow]) app-toolbar mwc-button {
width: 65px;
}
.heading {
overflow: hidden;
white-space: nowrap;
}
.heading .secondary {
color: var(--secondary-text-color);
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
}
`, `,
]; ];
} }

View File

@ -373,12 +373,27 @@
"video_not_supported": "Your browser does not support the video element.", "video_not_supported": "Your browser does not support the video element.",
"media_not_supported": "The Browser Media Player does not support this type of media", "media_not_supported": "The Browser Media Player does not support this type of media",
"media_browsing_error": "Media Browsing Error", "media_browsing_error": "Media Browsing Error",
"content-type": { "class": {
"server": "Server",
"library": "Library",
"artist": "Artist",
"album": "Album", "album": "Album",
"playlist": "Playlist" "app": "App",
"artist": "Artist",
"channel": "Channel",
"composer": "Composer",
"contributing_artist": "Contributing Artist",
"directory": "Library",
"episode": "Episode",
"game": "Game",
"genre": "Genre",
"image": "Image",
"movie": "Movie",
"music": "Music",
"playlist": "Playlist",
"podcast": "Podcast",
"season": "Season",
"track": "Track",
"tv_show": "TV Show",
"url": "Url",
"video": "Video"
} }
} }
}, },