diff --git a/build-scripts/webpack.js b/build-scripts/webpack.js
index 185ef3aa65..c5d741a5af 100644
--- a/build-scripts/webpack.js
+++ b/build-scripts/webpack.js
@@ -3,10 +3,10 @@ const webpack = require("webpack");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
-const paths = require("./paths.js");
-const bundle = require("./bundle.js");
const log = require("fancy-log");
const WebpackBar = require("webpackbar");
+const paths = require("./paths.js");
+const bundle = require("./bundle.js");
class LogStartCompilePlugin {
ignoredFirst = false;
@@ -138,6 +138,8 @@ const createWebpackConfig = ({
"lit/directives/cache$": "lit/directives/cache.js",
"lit/directives/repeat$": "lit/directives/repeat.js",
"lit/polyfill-support$": "lit/polyfill-support.js",
+ "@lit-labs/virtualizer/layouts/grid":
+ "@lit-labs/virtualizer/layouts/grid.js",
},
},
output: {
diff --git a/src/components/media-player/dialog-media-player-browse.ts b/src/components/media-player/dialog-media-player-browse.ts
index 01f7efbbd8..8a4f3f905e 100644
--- a/src/components/media-player/dialog-media-player-browse.ts
+++ b/src/components/media-player/dialog-media-player-browse.ts
@@ -151,6 +151,7 @@ class DialogMediaPlayerBrowse extends LitElement {
ha-media-player-browse {
--media-browser-max-height: calc(100vh - 65px);
+ height: calc(100vh - 65px);
}
@media (min-width: 800px) {
@@ -163,6 +164,7 @@ class DialogMediaPlayerBrowse extends LitElement {
ha-media-player-browse {
position: initial;
--media-browser-max-height: 100vh - 137px;
+ height: 100vh - 137px;
width: 700px;
}
}
diff --git a/src/components/media-player/ha-media-player-browse.ts b/src/components/media-player/ha-media-player-browse.ts
index 3de84c431d..5f219bf247 100644
--- a/src/components/media-player/ha-media-player-browse.ts
+++ b/src/components/media-player/ha-media-player-browse.ts
@@ -3,6 +3,8 @@ import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
+import { grid } from "@lit-labs/virtualizer/layouts/grid";
+import "@lit-labs/virtualizer";
import {
css,
CSSResultGroup,
@@ -16,16 +18,13 @@ import {
eventOptions,
property,
query,
- queryAll,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
-import { ifDefined } from "lit/directives/if-defined";
-import { styleMap } from "lit/directives/style-map";
+import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce";
-import { getSignedPath } from "../../data/auth";
import type { MediaPlayerItem } from "../../data/media-player";
import {
browseMediaPlayer,
@@ -40,18 +39,18 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
-import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
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-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 {
interface HASSDomEvents {
@@ -101,8 +100,6 @@ export class HaMediaPlayerBrowse extends LitElement {
@query(".content") private _content?: HTMLDivElement;
- @queryAll(".lazythumbnail") private _thumbnails?: HaCard[];
-
private _headerOffsetHeight = 0;
private _resizeObserver?: ResizeObserver;
@@ -148,326 +145,6 @@ export class HaMediaPlayerBrowse extends LitElement {
}
}
- protected render(): TemplateResult {
- if (this._error) {
- return html`
-
${this._renderError(this._error)}
- `;
- }
-
- if (!this._currentItem) {
- return html``;
- }
-
- 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` `
- : ""
- }
-
- ${
- this._error
- ? html`
-
- ${this._renderError(this._error)}
-
- `
- : isTTSMediaSource(currentItem.media_content_id)
- ? html`
-
- `
- : !children.length && !currentItem.not_shown
- ? html`
-
- ${currentItem.media_content_id ===
- "media-source://media_source/local/."
- ? html`
-
-
-
-
-
- ${this.hass.localize(
- "ui.components.media-browser.file_management.highlight_button"
- )}
-
-
- `
- : this.hass.localize(
- "ui.components.media-browser.no_items"
- )}
-
- `
- : childrenMediaClass.layout === "grid"
- ? html`
-
- `
- : html`
-
- ${children.map(
- (child) => html`
-
-
-
-
- ${child.title}
-
-
- `
- )}
- ${currentItem.not_shown
- ? html`
-
-
- ${this.hass.localize(
- "ui.components.media-browser.not_shown",
- { count: currentItem.not_shown }
- )}
-
-
- `
- : ""}
-
- `
- }
-
-
-
- `;
- }
-
- 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): void {
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 {
super.updated(changedProps);
@@ -590,16 +280,368 @@ export class HaMediaPlayerBrowse extends LitElement {
this._animateHeaderHeight();
} else if (changedProps.has("_currentItem")) {
this._setHeaderHeight();
- this._attachIntersectionObserver();
}
}
- private _actionClicked(ev: MouseEvent): void {
+ protected render(): TemplateResult {
+ if (this._error) {
+ return html`
+ ${this._renderError(this._error)}
+ `;
+ }
+
+ if (!this._currentItem) {
+ return html``;
+ }
+
+ 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`
+
+ `
+ : ""
+ }
+
+ ${
+ this._error
+ ? html`
+
+ ${this._renderError(this._error)}
+
+ `
+ : isTTSMediaSource(currentItem.media_content_id)
+ ? html`
+
+ `
+ : !children.length && !currentItem.not_shown
+ ? html`
+
+ ${currentItem.media_content_id ===
+ "media-source://media_source/local/."
+ ? html`
+
+
+
+
+
+ ${this.hass.localize(
+ "ui.components.media-browser.file_management.highlight_button"
+ )}
+
+
+ `
+ : this.hass.localize(
+ "ui.components.media-browser.no_items"
+ )}
+
+ `
+ : childrenMediaClass.layout === "grid"
+ ? html`
+
+ ${currentItem.not_shown
+ ? html`
+
+
+ ${this.hass.localize(
+ "ui.components.media-browser.not_shown",
+ { count: currentItem.not_shown }
+ )}
+
+
+ `
+ : ""}
+ `
+ : html`
+
+
+ ${currentItem.not_shown
+ ? html`
+
+
+ ${this.hass.localize(
+ "ui.components.media-browser.not_shown",
+ { count: currentItem.not_shown }
+ )}
+
+
+ `
+ : ""}
+
+ `
+ }
+
+
+
+ `;
+ }
+
+ private _renderGridItem = (child: MediaPlayerItem): TemplateResult => {
+ const backgroundImage = child.thumbnail
+ ? this._getSignedThumbnail(child.thumbnail).then(
+ (value) => `url(${value})`
+ )
+ : "none";
+
+ return html`
+
+
+
+ ${child.thumbnail
+ ? html`
+
+ `
+ : html`
+
+
+
+ `}
+ ${child.can_play
+ ? html`
+
+ `
+ : ""}
+
+
+ ${child.title}
+
${child.title}
+
+
+
+ `;
+ };
+
+ 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`
+
+
+
+
+ ${child.title}
+
+
+ `;
+ };
+
+ private async _getSignedThumbnail(
+ thumbnailUrl: string | undefined
+ ): Promise {
+ 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();
const item = (ev.currentTarget as any).item;
this._runAction(item);
- }
+ };
private _runAction(item: MediaPlayerItem): void {
fireEvent(this, "media-picked", { item, navigateIds: this.navigateIds });
@@ -615,7 +657,7 @@ export class HaMediaPlayerBrowse extends LitElement {
});
}
- private async _childClicked(ev: MouseEvent): Promise {
+ private _childClicked = async (ev: MouseEvent): Promise => {
const target = ev.currentTarget as any;
const item: MediaPlayerItem = target.item;
@@ -631,7 +673,7 @@ export class HaMediaPlayerBrowse extends LitElement {
fireEvent(this, "media-browsed", {
ids: [...this.navigateIds, item],
});
- }
+ };
private async _fetchData(
entityId: string,
@@ -658,55 +700,6 @@ export class HaMediaPlayerBrowse extends LitElement {
this._resizeObserver.observe(this);
}
- /**
- * Load thumbnails for images on demand as they become visible.
- */
- private async _attachIntersectionObserver(): Promise {
- 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 {
fireEvent(this, "close-dialog");
}
@@ -841,6 +834,7 @@ export class HaMediaPlayerBrowse extends LitElement {
.content {
overflow-y: auto;
box-sizing: border-box;
+ height: 100%;
}
/* HEADER */
@@ -926,6 +920,7 @@ export class HaMediaPlayerBrowse extends LitElement {
.not-shown {
font-style: italic;
color: var(--secondary-text-color);
+ padding: 8px 16px 8px;
}
.grid.not-shown {
@@ -951,7 +946,11 @@ export class HaMediaPlayerBrowse extends LitElement {
border-bottom-color: var(--divider-color);
}
- .children {
+ mwc-list-item {
+ width: 100%;
+ }
+
+ div.children {
display: grid;
grid-template-columns: repeat(
auto-fit,
@@ -988,7 +987,7 @@ export class HaMediaPlayerBrowse extends LitElement {
padding-bottom: 100%;
}
- .portrait.children ha-card .thumbnail {
+ .portrait ha-card .thumbnail {
padding-bottom: 150%;
}
@@ -1062,10 +1061,6 @@ export class HaMediaPlayerBrowse extends LitElement {
color: var(--primary-color);
}
- ha-card:hover .lazythumbnail {
- opacity: 0.5;
- }
-
.child .title {
font-size: 16px;
padding-top: 16px;
@@ -1127,7 +1122,7 @@ export class HaMediaPlayerBrowse extends LitElement {
padding: 0 24px;
}
- :host([narrow]) .children {
+ :host([narrow]) div.children {
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-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);
+ }
`,
];
}