mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 15:26:36 +00:00
Virtualize Media Player Grid (#11898)
This commit is contained in:
parent
3e188d1f87
commit
6b67546daf
@ -3,10 +3,10 @@ const webpack = require("webpack");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
||||||
const paths = require("./paths.js");
|
|
||||||
const bundle = require("./bundle.js");
|
|
||||||
const log = require("fancy-log");
|
const log = require("fancy-log");
|
||||||
const WebpackBar = require("webpackbar");
|
const WebpackBar = require("webpackbar");
|
||||||
|
const paths = require("./paths.js");
|
||||||
|
const bundle = require("./bundle.js");
|
||||||
|
|
||||||
class LogStartCompilePlugin {
|
class LogStartCompilePlugin {
|
||||||
ignoredFirst = false;
|
ignoredFirst = false;
|
||||||
@ -138,6 +138,8 @@ const createWebpackConfig = ({
|
|||||||
"lit/directives/cache$": "lit/directives/cache.js",
|
"lit/directives/cache$": "lit/directives/cache.js",
|
||||||
"lit/directives/repeat$": "lit/directives/repeat.js",
|
"lit/directives/repeat$": "lit/directives/repeat.js",
|
||||||
"lit/polyfill-support$": "lit/polyfill-support.js",
|
"lit/polyfill-support$": "lit/polyfill-support.js",
|
||||||
|
"@lit-labs/virtualizer/layouts/grid":
|
||||||
|
"@lit-labs/virtualizer/layouts/grid.js",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
@ -151,6 +151,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
|||||||
|
|
||||||
ha-media-player-browse {
|
ha-media-player-browse {
|
||||||
--media-browser-max-height: calc(100vh - 65px);
|
--media-browser-max-height: calc(100vh - 65px);
|
||||||
|
height: calc(100vh - 65px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
@media (min-width: 800px) {
|
||||||
@ -163,6 +164,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
|||||||
ha-media-player-browse {
|
ha-media-player-browse {
|
||||||
position: initial;
|
position: initial;
|
||||||
--media-browser-max-height: 100vh - 137px;
|
--media-browser-max-height: 100vh - 137px;
|
||||||
|
height: 100vh - 137px;
|
||||||
width: 700px;
|
width: 700px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ import "@material/mwc-list/mwc-list";
|
|||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
|
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
|
||||||
import "@polymer/paper-tooltip/paper-tooltip";
|
import "@polymer/paper-tooltip/paper-tooltip";
|
||||||
|
import { grid } from "@lit-labs/virtualizer/layouts/grid";
|
||||||
|
import "@lit-labs/virtualizer";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@ -16,16 +18,13 @@ import {
|
|||||||
eventOptions,
|
eventOptions,
|
||||||
property,
|
property,
|
||||||
query,
|
query,
|
||||||
queryAll,
|
|
||||||
state,
|
state,
|
||||||
} from "lit/decorators";
|
} from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { until } from "lit/directives/until";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
|
||||||
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";
|
||||||
import { getSignedPath } from "../../data/auth";
|
|
||||||
import type { MediaPlayerItem } from "../../data/media-player";
|
import type { MediaPlayerItem } from "../../data/media-player";
|
||||||
import {
|
import {
|
||||||
browseMediaPlayer,
|
browseMediaPlayer,
|
||||||
@ -40,18 +39,18 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
|||||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||||
import { haStyle } from "../../resources/styles";
|
import { haStyle } from "../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
|
|
||||||
import { documentationUrl } from "../../util/documentation-url";
|
import { documentationUrl } from "../../util/documentation-url";
|
||||||
import "../entity/ha-entity-picker";
|
import "../entity/ha-entity-picker";
|
||||||
import "../ha-button-menu";
|
import "../ha-button-menu";
|
||||||
import "../ha-card";
|
import "../ha-card";
|
||||||
import type { HaCard } from "../ha-card";
|
|
||||||
import "../ha-circular-progress";
|
import "../ha-circular-progress";
|
||||||
import "../ha-fab";
|
import "../ha-fab";
|
||||||
import "../ha-icon-button";
|
import "../ha-icon-button";
|
||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "./ha-browse-media-tts";
|
import "./ha-browse-media-tts";
|
||||||
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
|
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
|
||||||
|
import { getSignedPath } from "../../data/auth";
|
||||||
|
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
@ -101,8 +100,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
|
|
||||||
@query(".content") private _content?: HTMLDivElement;
|
@query(".content") private _content?: HTMLDivElement;
|
||||||
|
|
||||||
@queryAll(".lazythumbnail") private _thumbnails?: HaCard[];
|
|
||||||
|
|
||||||
private _headerOffsetHeight = 0;
|
private _headerOffsetHeight = 0;
|
||||||
|
|
||||||
private _resizeObserver?: ResizeObserver;
|
private _resizeObserver?: ResizeObserver;
|
||||||
@ -148,326 +145,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
if (this._error) {
|
|
||||||
return html`
|
|
||||||
<div class="container">${this._renderError(this._error)}</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._currentItem) {
|
|
||||||
return html`<ha-circular-progress active></ha-circular-progress>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentItem = this._currentItem;
|
|
||||||
|
|
||||||
const subtitle = this.hass.localize(
|
|
||||||
`ui.components.media-browser.class.${currentItem.media_class}`
|
|
||||||
);
|
|
||||||
const children = currentItem.children || [];
|
|
||||||
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
|
|
||||||
const childrenMediaClass = currentItem.children_media_class
|
|
||||||
? MediaClassBrowserSettings[currentItem.children_media_class]
|
|
||||||
: MediaClassBrowserSettings.directory;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
${
|
|
||||||
currentItem.can_play
|
|
||||||
? html` <div
|
|
||||||
class="header ${classMap({
|
|
||||||
"no-img": !currentItem.thumbnail,
|
|
||||||
"no-dialog": !this.dialog,
|
|
||||||
})}"
|
|
||||||
@transitionend=${this._setHeaderHeight}
|
|
||||||
>
|
|
||||||
<div class="header-content">
|
|
||||||
${currentItem.thumbnail
|
|
||||||
? html`
|
|
||||||
<div
|
|
||||||
class="img"
|
|
||||||
style=${styleMap({
|
|
||||||
backgroundImage: currentItem.thumbnail
|
|
||||||
? `url(${currentItem.thumbnail})`
|
|
||||||
: "none",
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
${this._narrow && currentItem?.can_play
|
|
||||||
? html`
|
|
||||||
<ha-fab
|
|
||||||
mini
|
|
||||||
.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}`
|
|
||||||
)}
|
|
||||||
</ha-fab>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: html``}
|
|
||||||
<div class="header-info">
|
|
||||||
<div class="breadcrumb">
|
|
||||||
<h1 class="title">${currentItem.title}</h1>
|
|
||||||
${subtitle
|
|
||||||
? html` <h2 class="subtitle">${subtitle}</h2> `
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
${currentItem.can_play &&
|
|
||||||
(!currentItem.thumbnail || !this._narrow)
|
|
||||||
? html`
|
|
||||||
<mwc-button
|
|
||||||
raised
|
|
||||||
.item=${currentItem}
|
|
||||||
@click=${this._actionClicked}
|
|
||||||
>
|
|
||||||
<ha-svg-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
|
|
||||||
class="content"
|
|
||||||
@scroll=${this._scroll}
|
|
||||||
@touchmove=${this._scroll}
|
|
||||||
>
|
|
||||||
${
|
|
||||||
this._error
|
|
||||||
? html`
|
|
||||||
<div class="container">
|
|
||||||
${this._renderError(this._error)}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: isTTSMediaSource(currentItem.media_content_id)
|
|
||||||
? html`
|
|
||||||
<ha-browse-media-tts
|
|
||||||
.item=${currentItem}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.action=${this.action}
|
|
||||||
@tts-picked=${this._ttsPicked}
|
|
||||||
></ha-browse-media-tts>
|
|
||||||
`
|
|
||||||
: !children.length && !currentItem.not_shown
|
|
||||||
? html`
|
|
||||||
<div class="container no-items">
|
|
||||||
${currentItem.media_content_id ===
|
|
||||||
"media-source://media_source/local/."
|
|
||||||
? html`
|
|
||||||
<div class="highlight-add-button">
|
|
||||||
<span>
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${mdiArrowUpRight}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.components.media-browser.file_management.highlight_button"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: this.hass.localize(
|
|
||||||
"ui.components.media-browser.no_items"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: childrenMediaClass.layout === "grid"
|
|
||||||
? html`
|
|
||||||
<div
|
|
||||||
class="children ${classMap({
|
|
||||||
portrait:
|
|
||||||
childrenMediaClass.thumbnail_ratio === "portrait",
|
|
||||||
})}"
|
|
||||||
>
|
|
||||||
${children.map(
|
|
||||||
(child) => html`
|
|
||||||
<div
|
|
||||||
class="child"
|
|
||||||
.item=${child}
|
|
||||||
@click=${this._childClicked}
|
|
||||||
>
|
|
||||||
<ha-card outlined>
|
|
||||||
<div class="thumbnail">
|
|
||||||
${child.thumbnail
|
|
||||||
? html`
|
|
||||||
<div
|
|
||||||
class="${["app", "directory"].includes(
|
|
||||||
child.media_class
|
|
||||||
)
|
|
||||||
? "centered-image"
|
|
||||||
: ""} image lazythumbnail"
|
|
||||||
data-src=${child.thumbnail}
|
|
||||||
></div>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div class="icon-holder image">
|
|
||||||
<ha-svg-icon
|
|
||||||
class="folder"
|
|
||||||
.path=${MediaClassBrowserSettings[
|
|
||||||
child.media_class === "directory"
|
|
||||||
? child.children_media_class ||
|
|
||||||
child.media_class
|
|
||||||
: child.media_class
|
|
||||||
].icon}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
${child.can_play
|
|
||||||
? html`
|
|
||||||
<ha-icon-button
|
|
||||||
class="play ${classMap({
|
|
||||||
can_expand: child.can_expand,
|
|
||||||
})}"
|
|
||||||
.item=${child}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
`ui.components.media-browser.${this.action}-media`
|
|
||||||
)}
|
|
||||||
.path=${this.action === "play"
|
|
||||||
? mdiPlay
|
|
||||||
: mdiPlus}
|
|
||||||
@click=${this._actionClicked}
|
|
||||||
></ha-icon-button>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
<div class="title">
|
|
||||||
${child.title}
|
|
||||||
<paper-tooltip
|
|
||||||
fitToVisibleBounds
|
|
||||||
position="top"
|
|
||||||
offset="4"
|
|
||||||
>${child.title}</paper-tooltip
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
${currentItem.not_shown
|
|
||||||
? html`
|
|
||||||
<div class="grid not-shown">
|
|
||||||
<div class="title">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.components.media-browser.not_shown",
|
|
||||||
{ count: currentItem.not_shown }
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<mwc-list>
|
|
||||||
${children.map(
|
|
||||||
(child) => html`
|
|
||||||
<mwc-list-item
|
|
||||||
@click=${this._childClicked}
|
|
||||||
.item=${child}
|
|
||||||
.graphic=${mediaClass.show_list_images
|
|
||||||
? "medium"
|
|
||||||
: "avatar"}
|
|
||||||
dir=${computeRTLDirection(this.hass)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class=${classMap({
|
|
||||||
graphic: true,
|
|
||||||
lazythumbnail:
|
|
||||||
mediaClass.show_list_images === true,
|
|
||||||
})}
|
|
||||||
data-src=${ifDefined(
|
|
||||||
mediaClass.show_list_images && child.thumbnail
|
|
||||||
? child.thumbnail
|
|
||||||
: undefined
|
|
||||||
)}
|
|
||||||
slot="graphic"
|
|
||||||
>
|
|
||||||
<ha-icon-button
|
|
||||||
class="play ${classMap({
|
|
||||||
show:
|
|
||||||
!mediaClass.show_list_images ||
|
|
||||||
!child.thumbnail,
|
|
||||||
})}"
|
|
||||||
.item=${child}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
`ui.components.media-browser.${this.action}-media`
|
|
||||||
)}
|
|
||||||
.path=${this.action === "play"
|
|
||||||
? mdiPlay
|
|
||||||
: mdiPlus}
|
|
||||||
@click=${this._actionClicked}
|
|
||||||
></ha-icon-button>
|
|
||||||
</div>
|
|
||||||
<span class="title">${child.title}</span>
|
|
||||||
</mwc-list-item>
|
|
||||||
<li divider role="separator"></li>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
${currentItem.not_shown
|
|
||||||
? html`
|
|
||||||
<mwc-list-item
|
|
||||||
noninteractive
|
|
||||||
class="not-shown"
|
|
||||||
.graphic=${mediaClass.show_list_images
|
|
||||||
? "medium"
|
|
||||||
: "avatar"}
|
|
||||||
dir=${computeRTLDirection(this.hass)}
|
|
||||||
>
|
|
||||||
<span class="title">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.components.media-browser.not_shown",
|
|
||||||
{ count: currentItem.not_shown }
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</mwc-list-item>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</mwc-list>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected firstUpdated(): void {
|
|
||||||
this._measureCard();
|
|
||||||
this._attachResizeObserver();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
|
||||||
if (changedProps.size > 1 || !changedProps.has("hass")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const oldHass = changedProps.get("hass") as this["hass"];
|
|
||||||
return oldHass === undefined || oldHass.localize !== this.hass.localize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public willUpdate(changedProps: PropertyValues<this>): void {
|
public willUpdate(changedProps: PropertyValues<this>): void {
|
||||||
super.willUpdate(changedProps);
|
super.willUpdate(changedProps);
|
||||||
|
|
||||||
@ -583,6 +260,19 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
|
if (changedProps.size > 1 || !changedProps.has("hass")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const oldHass = changedProps.get("hass") as this["hass"];
|
||||||
|
return oldHass === undefined || oldHass.localize !== this.hass.localize;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(): void {
|
||||||
|
this._measureCard();
|
||||||
|
this._attachResizeObserver();
|
||||||
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues): void {
|
protected updated(changedProps: PropertyValues): void {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
|
|
||||||
@ -590,16 +280,368 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
this._animateHeaderHeight();
|
this._animateHeaderHeight();
|
||||||
} else if (changedProps.has("_currentItem")) {
|
} else if (changedProps.has("_currentItem")) {
|
||||||
this._setHeaderHeight();
|
this._setHeaderHeight();
|
||||||
this._attachIntersectionObserver();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _actionClicked(ev: MouseEvent): void {
|
protected render(): TemplateResult {
|
||||||
|
if (this._error) {
|
||||||
|
return html`
|
||||||
|
<div class="container">${this._renderError(this._error)}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._currentItem) {
|
||||||
|
return html`<ha-circular-progress active></ha-circular-progress>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentItem = this._currentItem;
|
||||||
|
|
||||||
|
const subtitle = this.hass.localize(
|
||||||
|
`ui.components.media-browser.class.${currentItem.media_class}`
|
||||||
|
);
|
||||||
|
const children = currentItem.children || [];
|
||||||
|
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
|
||||||
|
const childrenMediaClass = currentItem.children_media_class
|
||||||
|
? MediaClassBrowserSettings[currentItem.children_media_class]
|
||||||
|
: MediaClassBrowserSettings.directory;
|
||||||
|
|
||||||
|
const backgroundImage = currentItem.thumbnail
|
||||||
|
? this._getSignedThumbnail(currentItem.thumbnail).then(
|
||||||
|
(value) => `url(${value})`
|
||||||
|
)
|
||||||
|
: "none";
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${
|
||||||
|
currentItem.can_play
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
class="header ${classMap({
|
||||||
|
"no-img": !currentItem.thumbnail,
|
||||||
|
"no-dialog": !this.dialog,
|
||||||
|
})}"
|
||||||
|
@transitionend=${this._setHeaderHeight}
|
||||||
|
>
|
||||||
|
<div class="header-content">
|
||||||
|
${currentItem.thumbnail
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
class="img"
|
||||||
|
style="background-image: ${until(
|
||||||
|
backgroundImage,
|
||||||
|
""
|
||||||
|
)}"
|
||||||
|
>
|
||||||
|
${this._narrow && currentItem?.can_play
|
||||||
|
? html`
|
||||||
|
<ha-fab
|
||||||
|
mini
|
||||||
|
.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}`
|
||||||
|
)}
|
||||||
|
</ha-fab>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html``}
|
||||||
|
<div class="header-info">
|
||||||
|
<div class="breadcrumb">
|
||||||
|
<h1 class="title">${currentItem.title}</h1>
|
||||||
|
${subtitle
|
||||||
|
? html` <h2 class="subtitle">${subtitle}</h2> `
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
${currentItem.can_play &&
|
||||||
|
(!currentItem.thumbnail || !this._narrow)
|
||||||
|
? html`
|
||||||
|
<mwc-button
|
||||||
|
raised
|
||||||
|
.item=${currentItem}
|
||||||
|
@click=${this._actionClicked}
|
||||||
|
>
|
||||||
|
<ha-svg-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
|
||||||
|
class="content"
|
||||||
|
@scroll=${this._scroll}
|
||||||
|
@touchmove=${this._scroll}
|
||||||
|
>
|
||||||
|
${
|
||||||
|
this._error
|
||||||
|
? html`
|
||||||
|
<div class="container">
|
||||||
|
${this._renderError(this._error)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: isTTSMediaSource(currentItem.media_content_id)
|
||||||
|
? html`
|
||||||
|
<ha-browse-media-tts
|
||||||
|
.item=${currentItem}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.action=${this.action}
|
||||||
|
@tts-picked=${this._ttsPicked}
|
||||||
|
></ha-browse-media-tts>
|
||||||
|
`
|
||||||
|
: !children.length && !currentItem.not_shown
|
||||||
|
? html`
|
||||||
|
<div class="container no-items">
|
||||||
|
${currentItem.media_content_id ===
|
||||||
|
"media-source://media_source/local/."
|
||||||
|
? html`
|
||||||
|
<div class="highlight-add-button">
|
||||||
|
<span>
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${mdiArrowUpRight}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.components.media-browser.file_management.highlight_button"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.components.media-browser.no_items"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: childrenMediaClass.layout === "grid"
|
||||||
|
? html`
|
||||||
|
<lit-virtualizer
|
||||||
|
scroller
|
||||||
|
.layout=${grid({
|
||||||
|
itemSize: {
|
||||||
|
width: "175px",
|
||||||
|
height: "225px",
|
||||||
|
},
|
||||||
|
gap: "16px",
|
||||||
|
flex: { preserve: "aspect-ratio" },
|
||||||
|
justify: "space-evenly",
|
||||||
|
direction: "vertical",
|
||||||
|
})}
|
||||||
|
.items=${children}
|
||||||
|
.renderItem=${this._renderGridItem}
|
||||||
|
class="children ${classMap({
|
||||||
|
portrait:
|
||||||
|
childrenMediaClass.thumbnail_ratio === "portrait",
|
||||||
|
not_shown: !!currentItem.not_shown,
|
||||||
|
})}"
|
||||||
|
></lit-virtualizer>
|
||||||
|
${currentItem.not_shown
|
||||||
|
? html`
|
||||||
|
<div class="grid not-shown">
|
||||||
|
<div class="title">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.components.media-browser.not_shown",
|
||||||
|
{ count: currentItem.not_shown }
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<mwc-list>
|
||||||
|
<lit-virtualizer
|
||||||
|
scroller
|
||||||
|
.items=${children}
|
||||||
|
.renderItem=${this._renderListItem}
|
||||||
|
></lit-virtualizer>
|
||||||
|
${currentItem.not_shown
|
||||||
|
? html`
|
||||||
|
<mwc-list-item
|
||||||
|
noninteractive
|
||||||
|
class="not-shown"
|
||||||
|
.graphic=${mediaClass.show_list_images
|
||||||
|
? "medium"
|
||||||
|
: "avatar"}
|
||||||
|
dir=${computeRTLDirection(this.hass)}
|
||||||
|
>
|
||||||
|
<span class="title">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.components.media-browser.not_shown",
|
||||||
|
{ count: currentItem.not_shown }
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</mwc-list-item>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</mwc-list>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderGridItem = (child: MediaPlayerItem): TemplateResult => {
|
||||||
|
const backgroundImage = child.thumbnail
|
||||||
|
? this._getSignedThumbnail(child.thumbnail).then(
|
||||||
|
(value) => `url(${value})`
|
||||||
|
)
|
||||||
|
: "none";
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="child" .item=${child} @click=${this._childClicked}>
|
||||||
|
<ha-card outlined>
|
||||||
|
<div class="thumbnail">
|
||||||
|
${child.thumbnail
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
class="${["app", "directory"].includes(child.media_class)
|
||||||
|
? "centered-image"
|
||||||
|
: ""} image"
|
||||||
|
style="background-image: ${until(backgroundImage, "")}"
|
||||||
|
></div>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<div class="icon-holder image">
|
||||||
|
<ha-svg-icon
|
||||||
|
class="folder"
|
||||||
|
.path=${MediaClassBrowserSettings[
|
||||||
|
child.media_class === "directory"
|
||||||
|
? child.children_media_class || child.media_class
|
||||||
|
: child.media_class
|
||||||
|
].icon}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
${child.can_play
|
||||||
|
? html`
|
||||||
|
<ha-icon-button
|
||||||
|
class="play ${classMap({
|
||||||
|
can_expand: child.can_expand,
|
||||||
|
})}"
|
||||||
|
.item=${child}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
`ui.components.media-browser.${this.action}-media`
|
||||||
|
)}
|
||||||
|
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||||||
|
@click=${this._actionClicked}
|
||||||
|
></ha-icon-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
<div class="title">
|
||||||
|
${child.title}
|
||||||
|
<paper-tooltip fitToVisibleBounds position="top" offset="4"
|
||||||
|
>${child.title}</paper-tooltip
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _renderListItem = (child: MediaPlayerItem): TemplateResult => {
|
||||||
|
const currentItem = this._currentItem;
|
||||||
|
const mediaClass = MediaClassBrowserSettings[currentItem!.media_class];
|
||||||
|
|
||||||
|
const backgroundImage =
|
||||||
|
mediaClass.show_list_images && child.thumbnail
|
||||||
|
? this._getSignedThumbnail(child.thumbnail).then(
|
||||||
|
(value) => `url(${value})`
|
||||||
|
)
|
||||||
|
: "none";
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<mwc-list-item
|
||||||
|
@click=${this._childClicked}
|
||||||
|
.item=${child}
|
||||||
|
.graphic=${mediaClass.show_list_images ? "medium" : "avatar"}
|
||||||
|
dir=${computeRTLDirection(this.hass)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class=${classMap({
|
||||||
|
graphic: true,
|
||||||
|
thumbnail: mediaClass.show_list_images === true,
|
||||||
|
})}
|
||||||
|
style="background-image: ${until(backgroundImage, "")}"
|
||||||
|
slot="graphic"
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
class="play ${classMap({
|
||||||
|
show: !mediaClass.show_list_images || !child.thumbnail,
|
||||||
|
})}"
|
||||||
|
.item=${child}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
`ui.components.media-browser.${this.action}-media`
|
||||||
|
)}
|
||||||
|
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||||||
|
@click=${this._actionClicked}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
<span class="title">${child.title}</span>
|
||||||
|
</mwc-list-item>
|
||||||
|
<li divider role="separator"></li>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private async _getSignedThumbnail(
|
||||||
|
thumbnailUrl: string | undefined
|
||||||
|
): Promise<string> {
|
||||||
|
if (!thumbnailUrl) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thumbnailUrl.startsWith("/")) {
|
||||||
|
// Thumbnails served by local API require authentication
|
||||||
|
return (await getSignedPath(this.hass, thumbnailUrl)).path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thumbnailUrl.startsWith("https://brands.home-assistant.io")) {
|
||||||
|
// The backend is not aware of the theme used by the users,
|
||||||
|
// so we rewrite the URL to show a proper icon
|
||||||
|
thumbnailUrl = brandsUrl({
|
||||||
|
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||||
|
type: "icon",
|
||||||
|
useFallback: true,
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return thumbnailUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _actionClicked = (ev: MouseEvent): void => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const item = (ev.currentTarget as any).item;
|
const item = (ev.currentTarget as any).item;
|
||||||
|
|
||||||
this._runAction(item);
|
this._runAction(item);
|
||||||
}
|
};
|
||||||
|
|
||||||
private _runAction(item: MediaPlayerItem): void {
|
private _runAction(item: MediaPlayerItem): void {
|
||||||
fireEvent(this, "media-picked", { item, navigateIds: this.navigateIds });
|
fireEvent(this, "media-picked", { item, navigateIds: this.navigateIds });
|
||||||
@ -615,7 +657,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _childClicked(ev: MouseEvent): Promise<void> {
|
private _childClicked = async (ev: MouseEvent): Promise<void> => {
|
||||||
const target = ev.currentTarget as any;
|
const target = ev.currentTarget as any;
|
||||||
const item: MediaPlayerItem = target.item;
|
const item: MediaPlayerItem = target.item;
|
||||||
|
|
||||||
@ -631,7 +673,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
fireEvent(this, "media-browsed", {
|
fireEvent(this, "media-browsed", {
|
||||||
ids: [...this.navigateIds, item],
|
ids: [...this.navigateIds, item],
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
private async _fetchData(
|
private async _fetchData(
|
||||||
entityId: string,
|
entityId: string,
|
||||||
@ -658,55 +700,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
this._resizeObserver.observe(this);
|
this._resizeObserver.observe(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Load thumbnails for images on demand as they become visible.
|
|
||||||
*/
|
|
||||||
private async _attachIntersectionObserver(): Promise<void> {
|
|
||||||
if (!("IntersectionObserver" in window) || !this._thumbnails) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this._intersectionObserver) {
|
|
||||||
this._intersectionObserver = new IntersectionObserver(
|
|
||||||
async (entries, observer) => {
|
|
||||||
await Promise.all(
|
|
||||||
entries.map(async (entry) => {
|
|
||||||
if (!entry.isIntersecting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const thumbnailCard = entry.target as HTMLElement;
|
|
||||||
let thumbnailUrl = thumbnailCard.dataset.src;
|
|
||||||
if (!thumbnailUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (thumbnailUrl.startsWith("/")) {
|
|
||||||
// Thumbnails served by local API require authentication
|
|
||||||
const signedPath = await getSignedPath(this.hass, thumbnailUrl);
|
|
||||||
thumbnailUrl = signedPath.path;
|
|
||||||
} else if (
|
|
||||||
thumbnailUrl.startsWith("https://brands.home-assistant.io")
|
|
||||||
) {
|
|
||||||
// The backend is not aware of the theme used by the users,
|
|
||||||
// so we rewrite the URL to show a proper icon
|
|
||||||
thumbnailUrl = brandsUrl({
|
|
||||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
|
||||||
type: "icon",
|
|
||||||
useFallback: true,
|
|
||||||
darkOptimized: this.hass.themes?.darkMode,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
thumbnailCard.style.backgroundImage = `url(${thumbnailUrl})`;
|
|
||||||
observer.unobserve(thumbnailCard); // loaded, so no need to observe anymore
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const observer = this._intersectionObserver!;
|
|
||||||
for (const thumbnailCard of this._thumbnails) {
|
|
||||||
observer.observe(thumbnailCard);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _closeDialogAction(): void {
|
private _closeDialogAction(): void {
|
||||||
fireEvent(this, "close-dialog");
|
fireEvent(this, "close-dialog");
|
||||||
}
|
}
|
||||||
@ -841,6 +834,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
.content {
|
.content {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HEADER */
|
/* HEADER */
|
||||||
@ -926,6 +920,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
.not-shown {
|
.not-shown {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
|
padding: 8px 16px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid.not-shown {
|
.grid.not-shown {
|
||||||
@ -951,7 +946,11 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
border-bottom-color: var(--divider-color);
|
border-bottom-color: var(--divider-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.children {
|
mwc-list-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.children {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(
|
grid-template-columns: repeat(
|
||||||
auto-fit,
|
auto-fit,
|
||||||
@ -988,7 +987,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
padding-bottom: 100%;
|
padding-bottom: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.portrait.children ha-card .thumbnail {
|
.portrait ha-card .thumbnail {
|
||||||
padding-bottom: 150%;
|
padding-bottom: 150%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1062,10 +1061,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-card:hover .lazythumbnail {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.child .title {
|
.child .title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
@ -1127,7 +1122,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([narrow]) .children {
|
:host([narrow]) div.children {
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1232,6 +1227,16 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
--mdc-fab-box-shadow: none;
|
--mdc-fab-box-shadow: none;
|
||||||
--mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5);
|
--mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lit-virtualizer {
|
||||||
|
height: 100%;
|
||||||
|
overflow: overlay !important;
|
||||||
|
contain: size layout !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
lit-virtualizer.not_shown {
|
||||||
|
height: calc(100% - 36px);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user