Media browser tweaks (#6720)

Co-authored-by: Zack Arnett <arnett.zackary@gmail.com>
This commit is contained in:
Bram Kragten 2020-08-31 23:30:41 +02:00 committed by GitHub
parent 4a176f1b43
commit eb12afe8cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 311 additions and 109 deletions

View File

@ -1,11 +1,11 @@
import "@material/mwc-dialog"; import "@material/mwc-dialog";
import type { Dialog } from "@material/mwc-dialog"; import type { Dialog } from "@material/mwc-dialog";
import { style } from "@material/mwc-dialog/mwc-dialog-css"; import { style } from "@material/mwc-dialog/mwc-dialog-css";
import "./ha-icon-button";
import { css, CSSResult, customElement, html } from "lit-element";
import type { Constructor, HomeAssistant } from "../types";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { css, CSSResult, customElement, html } from "lit-element";
import { computeRTLDirection } from "../common/util/compute_rtl"; import { computeRTLDirection } from "../common/util/compute_rtl";
import type { Constructor, HomeAssistant } from "../types";
import "./ha-icon-button";
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>; const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
@ -23,6 +23,10 @@ export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
@customElement("ha-dialog") @customElement("ha-dialog")
export class HaDialog extends MwcDialog { export class HaDialog extends MwcDialog {
public scrollToPos(x: number, y: number) {
this.contentElement.scrollTo(x, y);
}
protected renderHeading() { protected renderHeading() {
return html`<slot name="heading"> return html`<slot name="heading">
${super.renderHeading()} ${super.renderHeading()}
@ -62,6 +66,10 @@ export class HaDialog extends MwcDialog {
position: var(--dialog-surface-position, relative); position: var(--dialog-surface-position, relative);
min-height: var(--mdc-dialog-min-height, auto); min-height: var(--mdc-dialog-min-height, auto);
} }
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
display: flex;
flex-direction: column;
}
.header_button { .header_button {
position: absolute; position: absolute;
right: 16px; right: 16px;

View File

@ -15,7 +15,7 @@ import type {
} from "../../data/media-player"; } from "../../data/media-player";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { createCloseHeading } from "../ha-dialog"; import "../ha-dialog";
import "./ha-media-player-browse"; import "./ha-media-player-browse";
import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog"; import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog";
@ -56,18 +56,17 @@ class DialogMediaPlayerBrowse extends LitElement {
scrimClickAction scrimClickAction
escapeKeyAction escapeKeyAction
hideActions hideActions
.heading=${createCloseHeading( flexContent
this.hass,
this.hass.localize("ui.components.media-browser.media-player-browser")
)}
@closed=${this._closeDialog} @closed=${this._closeDialog}
> >
<ha-media-player-browse <ha-media-player-browse
dialog
.hass=${this.hass} .hass=${this.hass}
.entityId=${this._entityId} .entityId=${this._entityId}
.action=${this._action!} .action=${this._action!}
.mediaContentId=${this._mediaContentId} .mediaContentId=${this._mediaContentId}
.mediaContentType=${this._mediaContentType} .mediaContentType=${this._mediaContentType}
@close-dialog=${this._closeDialog}
@media-picked=${this._mediaPicked} @media-picked=${this._mediaPicked}
></ha-media-player-browse> ></ha-media-player-browse>
</ha-dialog> </ha-dialog>
@ -94,13 +93,20 @@ class DialogMediaPlayerBrowse extends LitElement {
--dialog-content-padding: 0; --dialog-content-padding: 0;
} }
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
border-bottom: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
}
@media (min-width: 800px) { @media (min-width: 800px) {
ha-dialog { ha-dialog {
--mdc-dialog-max-width: 800px; --mdc-dialog-max-width: 800px;
} }
ha-media-player-browse { ha-media-player-browse {
width: 700px; width: 700px;
padding: 20px 24px;
} }
} }
`, `,

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, mdiFolder, mdiPlay, mdiPlus } from "@mdi/js"; import { mdiArrowLeft, mdiClose, mdiFolder, 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 {
@ -16,8 +16,11 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one"; 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 { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
import { browseMediaPlayer, MediaPickedEvent } from "../../data/media-player"; import { browseMediaPlayer, MediaPickedEvent } from "../../data/media-player";
import type { MediaPlayerItem } from "../../data/media-player"; import type { MediaPlayerItem } from "../../data/media-player";
@ -49,6 +52,12 @@ export class HaMediaPlayerBrowse extends LitElement {
@property() public action: "pick" | "play" = "play"; @property() public action: "pick" | "play" = "play";
@property({ type: Boolean }) public hideBack = false;
@property({ type: Boolean }) public hideTitle = false;
@property({ type: Boolean }) public dialog = false;
@property({ type: Boolean, attribute: "narrow", reflect: true }) @property({ type: Boolean, attribute: "narrow", reflect: true })
private _narrow = false; private _narrow = false;
@ -69,6 +78,15 @@ export class HaMediaPlayerBrowse extends LitElement {
} }
} }
public navigateBack() {
this._mediaPlayerItems!.pop();
const item = this._mediaPlayerItems!.pop();
if (!item) {
return;
}
this._navigate(item);
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._mediaPlayerItems.length) { if (!this._mediaPlayerItems.length) {
return html``; return html``;
@ -90,8 +108,20 @@ export class HaMediaPlayerBrowse extends LitElement {
| MediaPlayerItem | MediaPlayerItem
| undefined = this._hasExpandableChildren(mostRecentItem.children); | undefined = this._hasExpandableChildren(mostRecentItem.children);
const showImages = mostRecentItem.children?.some(
(child) => child.thumbnail && child.thumbnail !== mostRecentItem.thumbnail
);
const mediaType = this.hass.localize(
`ui.components.media-browser.content-type.${mostRecentItem.media_content_type}`
);
return html` return html`
<div class="header"> <div
class="header ${classMap({
"no-img": !mostRecentItem.thumbnail,
})}"
>
<div class="header-content"> <div class="header-content">
${mostRecentItem.thumbnail ${mostRecentItem.thumbnail
? html` ? html`
@ -123,56 +153,65 @@ export class HaMediaPlayerBrowse extends LitElement {
` `
: html``} : html``}
<div class="header-info"> <div class="header-info">
<div class="breadcrumb-overflow"> ${this.hideTitle && (this._narrow || !mostRecentItem.thumbnail)
<div class="breadcrumb"> ? ""
${previousItem : html`<div class="breadcrumb-overflow">
? html` <div class="breadcrumb">
<div ${!this.hideBack && previousItem
class="previous-title" ? html`
.previous=${true} <div
.item=${previousItem} class="previous-title"
@click=${this._navigate} @click=${this.navigateBack}
> >
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon> <ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
${previousItem.title} ${previousItem.title}
</div> </div>
` `
: ""} : ""}
<h1 class="title">${mostRecentItem.title}</h1> <h1 class="title">${mostRecentItem.title}</h1>
<h2 class="subtitle"> ${mediaType
${this.hass.localize( ? html`<h2 class="subtitle">
`ui.components.media-browser.content-type.${mostRecentItem.media_content_type}` ${mediaType}
)} </h2>`
</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>`}
${mostRecentItem?.can_play &&
(!mostRecentItem.thumbnail || !this._narrow)
? html`
<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>
${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>
<div class="divider"></div>
${mostRecentItem.children?.length ${mostRecentItem.children?.length
? hasExpandableChildren ? hasExpandableChildren
? html` ? html`
@ -184,7 +223,7 @@ export class HaMediaPlayerBrowse extends LitElement {
<div <div
class="child" class="child"
.item=${child} .item=${child}
@click=${this._navigate} @click=${this._navigateForward}
> >
<div class="ha-card-parent"> <div class="ha-card-parent">
<ha-card <ha-card
@ -235,21 +274,41 @@ export class HaMediaPlayerBrowse extends LitElement {
: html` : html`
<mwc-list> <mwc-list>
${mostRecentItem.children.map( ${mostRecentItem.children.map(
(child) => html`<mwc-list-item (child) => html`
<mwc-list-item
@click=${this._actionClicked} @click=${this._actionClicked}
.item=${child} .item=${child}
graphic="icon" graphic="avatar"
hasMeta
> >
<span>${child.title}</span> <div
<ha-svg-icon class="graphic"
slot="graphic" style=${ifDefined(
.label=${this.hass.localize( showImages && child.thumbnail
`ui.components.media-browser.${this.action}-media` ? `background-image: url(${child.thumbnail})`
: undefined
)} )}
.path=${this.action === "play" ? mdiPlay : mdiPlus} slot="graphic"
></ha-svg-icon >
></mwc-list-item> <mwc-icon-button
<li divider role="separator"></li>` class="play ${classMap({
show: !showImages || !child.thumbnail,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
@click=${this._actionClicked}
>
<ha-svg-icon
.path=${this.action === "play" ? mdiPlay : mdiPlus}
></ha-svg-icon>
</mwc-icon-button>
</div>
<span>${child.title}</span>
</mwc-list-item>
<li divider role="separator"></li>
`
)} )}
</mwc-list> </mwc-list>
` `
@ -260,6 +319,11 @@ export class HaMediaPlayerBrowse extends LitElement {
protected firstUpdated(): void { protected firstUpdated(): void {
this._measureCard(); this._measureCard();
this._attachObserver(); this._attachObserver();
this.addEventListener("scroll", this._scroll, { passive: true });
this.addEventListener("touchmove", this._scroll, {
passive: true,
});
} }
protected updated(changedProps: PropertyValues): void { protected updated(changedProps: PropertyValues): void {
@ -295,25 +359,23 @@ export class HaMediaPlayerBrowse extends LitElement {
}); });
} }
private async _navigate(ev: MouseEvent): Promise<void> { private async _navigateForward(ev: MouseEvent): Promise<void> {
const target = ev.currentTarget as any; const target = ev.currentTarget as any;
let item: MediaPlayerItem | undefined; const item: MediaPlayerItem = target.item;
if (target.previous) {
this._mediaPlayerItems!.pop();
item = this._mediaPlayerItems!.pop();
}
item = target.item;
if (!item) { if (!item) {
return; return;
} }
this._navigate(item);
}
private async _navigate(item: MediaPlayerItem) {
const itemData = await this._fetchData( const itemData = await this._fetchData(
item.media_content_id, item.media_content_id,
item.media_content_type item.media_content_type
); );
this.scrollTo(0, 0);
this._mediaPlayerItems = [...this._mediaPlayerItems, itemData]; this._mediaPlayerItems = [...this._mediaPlayerItems, itemData];
} }
@ -332,7 +394,15 @@ export class HaMediaPlayerBrowse extends LitElement {
} }
private _measureCard(): void { private _measureCard(): void {
this._narrow = this.offsetWidth < 500; this._narrow = (this.dialog ? window.innerWidth : this.offsetWidth) < 450;
}
private _scroll(): void {
if (this.scrollTop > (this._narrow ? 224 : 125)) {
this.setAttribute("scroll", "");
} else if (this.scrollTop === 0) {
this.removeAttribute("scroll");
}
} }
private async _attachObserver(): Promise<void> { private async _attachObserver(): Promise<void> {
@ -350,22 +420,40 @@ export class HaMediaPlayerBrowse extends LitElement {
children.find((item: MediaPlayerItem) => item.can_expand) children.find((item: MediaPlayerItem) => item.can_expand)
); );
private _closeDialogAction(): void {
fireEvent(this, "close-dialog");
}
static get styles(): CSSResultArray { static get styles(): CSSResultArray {
return [ return [
haStyle, haStyle,
css` css`
:host { :host {
display: block; display: block;
overflow-y: auto;
display: flex;
padding: 0px 0px 20px;
flex-direction: column;
} }
.header { .header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
border-bottom: 1px solid var(--divider-color);
} }
.breadcrumb-overflow { .header_button {
display: flex; position: relative;
justify-content: space-between; top: 14px;
right: -8px;
}
.header {
background-color: var(--card-background-color);
position: sticky;
top: 0;
z-index: 5;
padding: 20px 24px 10px;
} }
.header-content { .header-content {
@ -380,6 +468,8 @@ export class HaMediaPlayerBrowse extends LitElement {
width: 200px; width: 200px;
margin-right: 16px; margin-right: 16px;
background-size: cover; background-size: cover;
border-radius: 4px;
transition: width 0.4s, height 0.4s;
} }
.header-info { .header-info {
@ -391,9 +481,14 @@ export class HaMediaPlayerBrowse extends LitElement {
flex: 1; flex: 1;
} }
.header-info .actions { .header-info mwc-button {
padding-top: 24px; display: block;
--mdc-theme-primary: var(--primary-color); }
.breadcrumb-overflow {
display: flex;
flex-grow: 1;
justify-content: space-between;
} }
.breadcrumb { .breadcrumb {
@ -404,7 +499,7 @@ export class HaMediaPlayerBrowse extends LitElement {
} }
.breadcrumb .title { .breadcrumb .title {
font-size: 48px; font-size: 32px;
line-height: 1.2; line-height: 1.2;
font-weight: bold; font-weight: bold;
margin: 0; margin: 0;
@ -412,6 +507,7 @@ export class HaMediaPlayerBrowse extends LitElement {
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
padding-right: 8px;
} }
.breadcrumb .previous-title { .breadcrumb .previous-title {
@ -428,17 +524,8 @@ export class HaMediaPlayerBrowse extends LitElement {
font-size: 16px; font-size: 16px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} margin-bottom: 0;
transition: height 0.5s, margin 0.5s;
.divider {
padding: 10px 0;
}
.divider::before {
height: 1px;
display: block;
background-color: var(--divider-color);
content: " ";
} }
/* ============= CHILDREN ============= */ /* ============= CHILDREN ============= */
@ -446,8 +533,7 @@ export class HaMediaPlayerBrowse extends LitElement {
mwc-list { mwc-list {
--mdc-list-vertical-padding: 0; --mdc-list-vertical-padding: 0;
--mdc-theme-text-icon-on-background: var(--secondary-text-color); --mdc-theme-text-icon-on-background: var(--secondary-text-color);
border: 1px solid var(--divider-color); margin-top: 10px;
border-radius: 4px;
} }
mwc-list li:last-child { mwc-list li:last-child {
@ -468,6 +554,10 @@ export class HaMediaPlayerBrowse extends LitElement {
margin: 8px 0px; margin: 8px 0px;
} }
:host(:not([narrow])) .children {
padding: 0px 24px;
}
.child { .child {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -505,7 +595,7 @@ export class HaMediaPlayerBrowse extends LitElement {
bottom: 4px; bottom: 4px;
right: 4px; right: 4px;
transition: all 0.5s; transition: all 0.5s;
background-color: rgba(255, 255, 255, 0.5); background-color: rgba(var(--rgb-card-background-color), 0.5);
border-radius: 50%; border-radius: 50%;
} }
@ -531,22 +621,46 @@ export class HaMediaPlayerBrowse extends LitElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
mwc-list-item .graphic {
background-size: cover;
}
mwc-list-item .graphic .play {
opacity: 0;
transition: all 0.5s;
background-color: rgba(var(--rgb-card-background-color), 0.5);
border-radius: 50%;
--mdc-icon-button-size: 40px;
}
mwc-list-item:hover .graphic .play {
opacity: 1;
color: var(--primary-color);
}
mwc-list-item .graphic .play.show {
opacity: 1;
background-color: transparent;
}
/* ============= Narrow ============= */ /* ============= Narrow ============= */
:host([narrow]) { :host([narrow]) {
padding: 0; padding: 0;
} }
:host([narrow]) mwc-list {
border: 0;
}
:host([narrow]) .breadcrumb .title { :host([narrow]) .breadcrumb .title {
font-size: 38px; font-size: 24px;
} }
:host([narrow]) .breadcrumb-overflow { :host([narrow]) .header {
align-items: flex-end; padding: 0;
}
:host([narrow]) .header_button {
position: absolute;
top: 14px;
right: 8px;
} }
:host([narrow]) .header-content { :host([narrow]) .header-content {
@ -558,26 +672,100 @@ export class HaMediaPlayerBrowse extends LitElement {
height: auto; height: auto;
width: 100%; width: 100%;
margin-right: 0; margin-right: 0;
padding-bottom: 100%; padding-bottom: 50%;
margin-bottom: 8px; margin-bottom: 8px;
position: relative; position: relative;
background-position: center;
border-radius: 0;
transition: width 0.4s, height 0.4s, padding-bottom 0.4s;
} }
:host([narrow]) .header-content .img mwc-fab { mwc-fab {
position: absolute; position: absolute;
--mdc-theme-secondary: var(--primary-color); --mdc-theme-secondary: var(--primary-color);
bottom: -20px; bottom: -20px;
right: 20px; right: 20px;
} }
:host([narrow]) .header-info, :host([narrow]) .header-info mwc-button {
margin-top: 16px;
margin-bottom: 8px;
}
:host([narrow]) .header-info {
padding: 20px 24px 10px;
}
:host([narrow]) .media-source, :host([narrow]) .media-source,
:host([narrow]) .children { :host([narrow]) .children {
padding: 0 24px; padding: 0 24px;
} }
:host([narrow]) .children { :host([narrow]) .children {
grid-template-columns: 1fr 1fr !important; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
}
/* ============= Scroll ============= */
:host([scroll]) .breadcrumb .subtitle {
height: 0;
margin: 0;
}
:host([scroll]) .breadcrumb .title {
-webkit-line-clamp: 1;
}
:host([scroll]) .header-info mwc-button,
.no-img .header-info mwc-button {
padding-right: 4px;
}
:host([scroll][narrow]) .no-img .header-info mwc-button {
padding-right: 16px;
}
:host([scroll]) .header-info {
flex-direction: row;
}
:host([scroll]) .header-info mwc-button {
align-self: center;
margin-top: 0;
margin-bottom: 0;
}
:host([scroll][narrow]) .no-img .header-info {
flex-direction: row-reverse;
}
:host([scroll][narrow]) .header-info {
padding: 20px 24px 10px 24px;
align-items: center;
}
:host([scroll]) .header-content {
align-items: flex-end;
flex-direction: row;
}
:host([scroll]) .header-content .img {
height: 75px;
width: 75px;
}
:host([scroll][narrow]) .header-content .img {
height: 100px;
width: 100px;
padding-bottom: initial;
margin-bottom: 0;
}
:host([scroll]) mwc-fab {
bottom: 4px;
right: 4px;
--mdc-fab-box-shadow: none;
--mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5);
} }
`, `,
]; ];