diff --git a/src/components/media-player/dialog-media-player-browse.ts b/src/components/media-player/dialog-media-player-browse.ts index 791aee67d5..8e737dc0ed 100644 --- a/src/components/media-player/dialog-media-player-browse.ts +++ b/src/components/media-player/dialog-media-player-browse.ts @@ -1,9 +1,13 @@ +import "../ha-header-bar"; +import { mdiArrowLeft, mdiClose } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event"; +import { computeRTLDirection } from "../../common/util/compute_rtl"; import type { MediaPickedEvent, MediaPlayerBrowseAction, + MediaPlayerItem, } from "../../data/media-player"; import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; @@ -16,6 +20,8 @@ import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog"; class DialogMediaPlayerBrowse extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; + @state() private _currentItem?: MediaPlayerItem; + @state() private _navigateIds?: MediaPlayerItemId[]; @state() private _params?: MediaPlayerBrowseDialogParams; @@ -33,11 +39,12 @@ class DialogMediaPlayerBrowse extends LitElement { public closeDialog() { this._params = undefined; this._navigateIds = undefined; + this._currentItem = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render(): TemplateResult { - if (!this._params) { + if (!this._params || !this._navigateIds) { return html``; } @@ -48,8 +55,36 @@ class DialogMediaPlayerBrowse extends LitElement { escapeKeyAction hideActions flexContent + .heading=${true} @closed=${this.closeDialog} > + + ${this._navigateIds.length > 1 + ? html` + + ` + : ""} + + ${!this._currentItem + ? this.hass.localize( + "ui.components.media-browser.media-player-browser" + ) + : this._currentItem.title} + + + + ): void { @@ -89,7 +130,7 @@ class DialogMediaPlayerBrowse extends LitElement { } ha-media-player-browse { - --media-browser-max-height: 100vh; + --media-browser-max-height: calc(100vh - 65px); } @media (min-width: 800px) { @@ -101,10 +142,17 @@ class DialogMediaPlayerBrowse extends LitElement { } ha-media-player-browse { position: initial; - --media-browser-max-height: 100vh - 72px; + --media-browser-max-height: 100vh - 137px; width: 700px; } } + + 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(--divider-color, rgba(0, 0, 0, 0.12)); + } `, ]; } diff --git a/src/components/media-player/ha-media-player-browse.ts b/src/components/media-player/ha-media-player-browse.ts index d9006d9d1f..2f8e21686a 100644 --- a/src/components/media-player/ha-media-player-browse.ts +++ b/src/components/media-player/ha-media-player-browse.ts @@ -1,7 +1,7 @@ import "@material/mwc-button/mwc-button"; import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list-item"; -import { mdiArrowLeft, mdiClose, mdiPlay, mdiPlus } from "@mdi/js"; +import { mdiPlay, mdiPlus } from "@mdi/js"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-tooltip/paper-tooltip"; @@ -44,16 +44,17 @@ import type { HomeAssistant } from "../../types"; import { documentationUrl } from "../../util/documentation-url"; import "../entity/ha-entity-picker"; import "../ha-button-menu"; +import "../ha-card"; import type { HaCard } from "../ha-card"; import "../ha-circular-progress"; -import "../ha-fab"; import "../ha-icon-button"; import "../ha-svg-icon"; +import "../ha-fab"; declare global { interface HASSDomEvents { "media-picked": MediaPickedEvent; - "media-browsed": { ids: MediaPlayerItemId[]; back?: boolean }; + "media-browsed": { ids: MediaPlayerItemId[]; current?: MediaPlayerItem }; } } @@ -72,14 +73,15 @@ export class HaMediaPlayerBrowse extends LitElement { @property({ type: Boolean }) public dialog = false; + @property() public navigateIds!: MediaPlayerItemId[]; + @property({ type: Boolean, attribute: "narrow", reflect: true }) + // @ts-ignore private _narrow = false; @property({ type: Boolean, attribute: "scroll", reflect: true }) private _scrolled = false; - @property() public navigateIds!: MediaPlayerItemId[]; - @state() private _error?: { message: string; code: string }; @state() private _parentItem?: MediaPlayerItem; @@ -113,6 +115,12 @@ export class HaMediaPlayerBrowse extends LitElement { } } + public play(): void { + if (this._currentItem?.can_play) { + this._runAction(this._currentItem); + } + } + protected render(): TemplateResult { if (this._error) { return html` @@ -129,143 +137,207 @@ export class HaMediaPlayerBrowse extends LitElement { const subtitle = this.hass.localize( `ui.components.media-browser.class.${currentItem.media_class}` ); + const mediaClass = MediaClassBrowserSettings[currentItem.media_class]; const childrenMediaClass = MediaClassBrowserSettings[currentItem.children_media_class]; return html` -
-
- ${currentItem.thumbnail - ? html` -
- ${this._narrow && currentItem?.can_play - ? html` - - - ${this.hass.localize( - `ui.components.media-browser.${this.action}` - )} - - ` - : ""} -
- ` - : html``} -
- - ${this.dialog - ? html` - - ` - : ""} -
-
- ${this._error - ? html` -
${this._renderError(this._error)}
- ` - : currentItem.children?.length - ? childrenMediaClass.layout === "grid" - ? html` -
- ${currentItem.children.map( - (child) => html` + : currentItem.children?.length + ? childrenMediaClass.layout === "grid" + ? html`
-
- - ${!child.thumbnail - ? html` - - ` - : ""} - - ${child.can_play - ? html` + ${currentItem.children.map( + (child) => html` +
+ +
+ ${child.thumbnail + ? html` +
+ ` + : html` +
+ +
+ `} + ${child.can_play + ? html` + + ` + : ""} +
+
+ ${child.title} + ${child.title} +
+
+
+ ` + )} +
+ ` + : html` + + ${currentItem.children.map( + (child) => html` + +
- ` - : ""} -
-
- ${child.title} - ${child.title} -
-
- ${this.hass.localize( - `ui.components.media-browser.content-type.${child.media_content_type}` - )} -
-
+
+ ${child.title} + +
  • + ` + )} + ` - )} -
    - ` - : html` - - ${currentItem.children.map( - (child) => html` - -
    - -
    - ${child.title} -
    -
  • - ` - )} -
    - ` - : html` -
    - ${this.hass.localize("ui.components.media-browser.no_items")} -
    - ${currentItem.media_content_id === - "media-source://media_source/local/." - ? html`
    ${this.hass.localize( - "ui.components.media-browser.learn_adding_local_media", - "documentation", - html`${this.hass.localize( - "ui.components.media-browser.documentation" - )}` + : html` +
    + ${this.hass.localize( + "ui.components.media-browser.no_items" )}
    - ${this.hass.localize( - "ui.components.media-browser.local_media_files" - )}` - : ""} -
    - `} + ${currentItem.media_content_id === + "media-source://media_source/local/." + ? html`
    ${this.hass.localize( + "ui.components.media-browser.learn_adding_local_media", + "documentation", + html`${this.hass.localize( + "ui.components.media-browser.documentation" + )}` + )} +
    + ${this.hass.localize( + "ui.components.media-browser.local_media_files" + )}` + : ""} +
    + ` + } +
    +
    `; } @@ -453,6 +475,10 @@ export class HaMediaPlayerBrowse extends LitElement { currentProm.then( (item) => { this._currentItem = item; + fireEvent(this, "media-browsed", { + ids: this.navigateIds, + current: item, + }); }, (err) => this._setError(err) ); @@ -482,40 +508,6 @@ export class HaMediaPlayerBrowse extends LitElement { } } - private navigateBack() { - fireEvent(this, "media-browsed", { - ids: this.navigateIds.slice(0, -1), - back: true, - }); - } - - private async _setHeaderHeight() { - await this.updateComplete; - const header = this._header; - const content = this._content; - if (!header || !content) { - return; - } - this._headerOffsetHeight = header.offsetHeight; - content.style.marginTop = `${this._headerOffsetHeight}px`; - content.style.maxHeight = `calc(var(--media-browser-max-height, 100%) - ${this._headerOffsetHeight}px)`; - } - - private _animateHeaderHeight() { - let start; - const animate = (time) => { - if (start === undefined) { - start = time; - } - const elapsed = time - start; - this._setHeaderHeight(); - if (elapsed < 400) { - requestAnimationFrame(animate); - } - }; - requestAnimationFrame(animate); - } - private _actionClicked(ev: MouseEvent): void { ev.stopPropagation(); const item = (ev.currentTarget as any).item; @@ -559,16 +551,6 @@ export class HaMediaPlayerBrowse extends LitElement { this._narrow = (this.dialog ? window.innerWidth : this.offsetWidth) < 450; } - @eventOptions({ passive: true }) - private _scroll(ev: Event): void { - const content = ev.currentTarget as HTMLDivElement; - if (!this._scrolled && content.scrollTop > this._headerOffsetHeight) { - this._scrolled = true; - } else if (this._scrolled && content.scrollTop < this._headerOffsetHeight) { - this._scrolled = false; - } - } - private async _attachResizeObserver(): Promise { if (!this._resizeObserver) { await installResizeObserver(); @@ -595,7 +577,7 @@ export class HaMediaPlayerBrowse extends LitElement { if (!entry.isIntersecting) { return; } - const thumbnailCard = entry.target as HaCard; + const thumbnailCard = entry.target as HTMLElement; let thumbnailUrl = thumbnailCard.dataset.src; if (!thumbnailUrl) { return; @@ -675,6 +657,43 @@ export class HaMediaPlayerBrowse extends LitElement { return html`${err.message}`; } + private async _setHeaderHeight() { + await this.updateComplete; + const header = this._header; + const content = this._content; + if (!header || !content) { + return; + } + this._headerOffsetHeight = header.offsetHeight; + content.style.marginTop = `${this._headerOffsetHeight}px`; + content.style.maxHeight = `calc(var(--media-browser-max-height, 100%) - ${this._headerOffsetHeight}px)`; + } + + private _animateHeaderHeight() { + let start; + const animate = (time) => { + if (start === undefined) { + start = time; + } + const elapsed = time - start; + this._setHeaderHeight(); + if (elapsed < 400) { + requestAnimationFrame(animate); + } + }; + requestAnimationFrame(animate); + } + + @eventOptions({ passive: true }) + private _scroll(ev: Event): void { + const content = ev.currentTarget as HTMLDivElement; + if (!this._scrolled && content.scrollTop > this._headerOffsetHeight) { + this._scrolled = true; + } else if (this._scrolled && content.scrollTop < this._headerOffsetHeight) { + this._scrolled = false; + } + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -702,10 +721,11 @@ export class HaMediaPlayerBrowse extends LitElement { .content { overflow-y: auto; - padding-bottom: 20px; box-sizing: border-box; } + /* HEADER */ + .header { display: flex; justify-content: space-between; @@ -716,30 +736,26 @@ export class HaMediaPlayerBrowse extends LitElement { right: 0; left: 0; z-index: 5; - padding: 20px 24px 10px 32px; + padding: 16px; } - .header_button { position: relative; right: -8px; } - .header-content { display: flex; flex-wrap: wrap; flex-grow: 1; align-items: flex-start; } - .header-content .img { - height: 200px; - width: 200px; + height: 175px; + width: 175px; margin-right: 16px; background-size: cover; - border-radius: 4px; + border-radius: 2px; transition: width 0.4s, height 0.4s; } - .header-info { display: flex; flex-direction: column; @@ -748,19 +764,18 @@ export class HaMediaPlayerBrowse extends LitElement { min-width: 0; flex: 1; } - .header-info mwc-button { display: block; --mdc-theme-primary: var(--primary-color); + padding-bottom: 16px; } - .breadcrumb { display: flex; flex-direction: column; overflow: hidden; flex-grow: 1; + padding-top: 16px; } - .breadcrumb .title { font-size: 32px; line-height: 1.2; @@ -772,7 +787,6 @@ export class HaMediaPlayerBrowse extends LitElement { -webkit-line-clamp: 2; padding-right: 8px; } - .breadcrumb .previous-title { font-size: 14px; padding-bottom: 8px; @@ -782,7 +796,6 @@ export class HaMediaPlayerBrowse extends LitElement { cursor: pointer; --mdc-icon-size: 14px; } - .breadcrumb .subtitle { font-size: 16px; overflow: hidden; @@ -815,7 +828,7 @@ export class HaMediaPlayerBrowse extends LitElement { minmax(var(--media-browse-item-size, 175px), 0.1fr) ); grid-gap: 16px; - padding: 8px; + padding: 16px; } :host([dialog]) .children { @@ -831,42 +844,60 @@ export class HaMediaPlayerBrowse extends LitElement { cursor: pointer; } - .ha-card-parent { + ha-card { position: relative; width: 100%; + box-sizing: border-box; } - .children ha-card { + .children ha-card .thumbnail { width: 100%; - padding-bottom: 100%; position: relative; box-sizing: border-box; - background-size: cover; - background-repeat: no-repeat; - background-position: center; transition: padding-bottom 0.1s ease-out; + padding-bottom: 100%; } - .portrait.children ha-card { + .portrait.children ha-card .thumbnail { padding-bottom: 150%; } - .child .folder, - .child .play { + ha-card .image { + border-radius: 3px 3px 0 0; + } + + .image { position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + background-size: cover; + background-repeat: no-repeat; + background-position: center; + } + + .centered-image { + margin: 0 8px; + background-size: contain; + } + + .children ha-card .icon-holder { + display: flex; + justify-content: center; + align-items: center; } .child .folder { color: var(--secondary-text-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 { + position: absolute; transition: color 0.5s; border-radius: 50%; - bottom: calc(50% - 35px); + top: calc(50% - 50px); right: calc(50% - 35px); opacity: 0; transition: opacity 0.1s ease-out; @@ -877,45 +908,57 @@ export class HaMediaPlayerBrowse extends LitElement { --mdc-icon-size: 48px; } - .ha-card-parent:hover .play:not(.can_expand) { + ha-card:hover .play { opacity: 1; + } + + ha-card:hover .play:not(.can_expand) { color: var(--primary-color); } + ha-card:hover .play.can_expand { + bottom: 8px; + } + .child .play.can_expand { - opacity: 1; background-color: rgba(var(--rgb-card-background-color), 0.5); - bottom: 4px; - right: 4px; + top: auto; + bottom: 0px; + right: 8px; + transition: bottom 0.1s ease-out, opacity 0.1s ease-out; } .child .play:hover { color: var(--primary-color); } - .ha-card-parent:hover ha-card { + ha-card:hover .lazythumbnail { opacity: 0.5; } .child .title { font-size: 16px; - padding-top: 8px; + padding-top: 16px; padding-left: 2px; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; - -webkit-line-clamp: 2; + -webkit-line-clamp: 1; text-overflow: ellipsis; } - .child .type { - font-size: 12px; - color: var(--secondary-text-color); - padding-left: 2px; + .child ha-card .title { + margin-bottom: 16px; + padding-left: 16px; } mwc-list-item .graphic { - background-size: cover; + background-size: contain; + border-radius: 2px; + display: flex; + align-content: center; + align-items: center; + line-height: initial; } mwc-list-item .graphic .play { @@ -928,7 +971,7 @@ export class HaMediaPlayerBrowse extends LitElement { mwc-list-item:hover .graphic .play { opacity: 1; - color: var(--primary-color); + color: var(--primary-text-color); } mwc-list-item .graphic .play.show { @@ -950,29 +993,32 @@ export class HaMediaPlayerBrowse extends LitElement { padding: 0; } + :host([narrow]) .media-source { + padding: 0 24px; + } + + :host([narrow]) .children { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important; + } + :host([narrow]) .breadcrumb .title { font-size: 24px; } - :host([narrow]) .header { padding: 0; } - :host([narrow]) .header.no-dialog { display: block; } - :host([narrow]) .header_button { position: absolute; top: 14px; right: 8px; } - :host([narrow]) .header-content { flex-direction: column; flex-wrap: nowrap; } - :host([narrow]) .header-content .img { height: auto; width: 100%; @@ -984,94 +1030,75 @@ export class HaMediaPlayerBrowse extends LitElement { border-radius: 0; transition: width 0.4s, height 0.4s, padding-bottom 0.4s; } - ha-fab { position: absolute; --mdc-theme-secondary: var(--primary-color); bottom: -20px; right: 20px; } - :host([narrow]) .header-info mwc-button { margin-top: 16px; margin-bottom: 8px; } - :host([narrow]) .header-info { - padding: 20px 24px 10px; - } - - :host([narrow]) .media-source { - padding: 0 24px; - } - - :host([narrow]) .children { - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important; + padding: 0 16px 8px; } /* ============= Scroll ============= */ - :host([scroll]) .breadcrumb .subtitle { height: 0; margin: 0; } - :host([scroll]) .breadcrumb .title { -webkit-line-clamp: 1; } - :host(:not([narrow])[scroll]) .header:not(.no-img) ha-icon-button { align-self: center; } - :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; + padding-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]) .breadcrumb { + padding-top: 0; + align-self: center; + } :host([scroll][narrow]) .header-content .img { height: 100px; width: 100px; padding-bottom: initial; margin-bottom: 0; } - :host([scroll]) ha-fab { - bottom: 4px; - right: 4px; + bottom: 0px; + right: -24px; --mdc-fab-box-shadow: none; --mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5); } diff --git a/src/data/media-player.ts b/src/data/media-player.ts index 4f622ddeb7..ee15481785 100644 --- a/src/data/media-player.ts +++ b/src/data/media-player.ts @@ -88,7 +88,7 @@ export const BROWSER_PLAYER = "browser"; export type MediaClassBrowserSetting = { icon: string; thumbnail_ratio?: string; - layout?: string; + layout?: "grid"; show_list_images?: boolean; }; diff --git a/src/panels/media-browser/ha-bar-media-player.ts b/src/panels/media-browser/ha-bar-media-player.ts index 34c5e77156..0c760dadee 100644 --- a/src/panels/media-browser/ha-bar-media-player.ts +++ b/src/panels/media-browser/ha-bar-media-player.ts @@ -19,6 +19,7 @@ import { TemplateResult, } from "lit"; import { customElement, property, query, state } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; @@ -152,7 +153,10 @@ class BarMediaPlayer extends LitElement { stateObj.attributes.entity_picture; return html` -
    +
    ${mediaArt ? html`` : ""}
    - + ${this._navigateIds.length > 1 + ? html` + + ` + : html` + + `}
    - ${this.hass.localize( - "ui.components.media-browser.media-player-browser" - )} + ${!this._currentItem + ? this.hass.localize( + "ui.components.media-browser.media-player-browser" + ) + : this._currentItem.title}
    @@ -110,13 +129,19 @@ class PanelMediaBrowser extends LitElement { }; }), ]; + this._currentItem = undefined; } - private _mediaBrowsed(ev) { - if (ev.detail.back) { - history.back(); + private _goBack() { + history.back(); + } + + private _mediaBrowsed(ev: { detail: HASSDomEvents["media-browsed"] }) { + if (ev.detail.ids === this._navigateIds) { + this._currentItem = ev.detail.current; return; } + let path = ""; for (const item of ev.detail.ids.slice(1)) { path += @@ -152,6 +177,7 @@ class PanelMediaBrowser extends LitElement { sourceUrl: resolvedUrl.url, sourceType: resolvedUrl.mime_type, title: item.title, + can_play: item.can_play, }); } } diff --git a/src/panels/media-browser/show-media-player-dialog.ts b/src/panels/media-browser/show-media-player-dialog.ts index 2212c002fc..0600f2230e 100644 --- a/src/panels/media-browser/show-media-player-dialog.ts +++ b/src/panels/media-browser/show-media-player-dialog.ts @@ -4,6 +4,7 @@ export interface WebBrowserPlayMediaDialogParams { sourceUrl: string; sourceType: string; title?: string; + can_play?: boolean; } export const showWebBrowserPlayMediaDialog = (