import type { LitVirtualizer } from "@lit-labs/virtualizer"; import { grid } from "@lit-labs/virtualizer/layouts/grid"; import { mdiArrowUpRight, mdiKeyboard, mdiPlay, mdiPlus } from "@mdi/js"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, eventOptions, property, query, state, } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { styleMap } from "lit/directives/style-map"; import { until } from "lit/directives/until"; import { fireEvent } from "../../common/dom/fire_event"; import { debounce } from "../../common/util/debounce"; import { isUnavailableState } from "../../data/entity"; import type { MediaPickedEvent, MediaPlayerBrowseAction, MediaPlayerItem, MediaPlayerLayoutType, } from "../../data/media-player"; import { browseMediaPlayer, BROWSER_PLAYER, MediaClassBrowserSettings, } from "../../data/media-player"; import { browseLocalMediaPlayer, isManualMediaSourceContentId, MANUAL_MEDIA_SOURCE_PREFIX, } from "../../data/media_source"; import { isTTSMediaSource } from "../../data/tts"; import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../resources/styles"; import { loadVirtualizer } from "../../resources/virtualizer"; import type { HomeAssistant } from "../../types"; import { brandsUrl, extractDomainFromBrandUrl, isBrandUrl, } from "../../util/brands-url"; import { documentationUrl } from "../../util/documentation-url"; import "../entity/ha-entity-picker"; import "../ha-alert"; import "../ha-button"; import "../ha-button-menu"; import "../ha-card"; import "../ha-fab"; import "../ha-icon-button"; import "../ha-list"; import "../ha-list-item"; import "../ha-spinner"; import "../ha-svg-icon"; import "../ha-tooltip"; import "./ha-browse-media-manual"; import type { ManualMediaPickedEvent } from "./ha-browse-media-manual"; import "./ha-browse-media-tts"; import type { TtsMediaPickedEvent } from "./ha-browse-media-tts"; declare global { interface HASSDomEvents { "media-picked": MediaPickedEvent; "media-browsed": { // Items of the new browse stack ids: MediaPlayerItemId[]; // Current fetched item for this browse stack current?: MediaPlayerItem; // If the new stack should replace the old stack replace?: boolean; }; } } export interface MediaPlayerItemId { media_content_id?: string | undefined; media_content_type?: string | undefined; } const MANUAL_ITEM: MediaPlayerItem = { can_expand: true, can_play: false, can_search: false, children_media_class: "", media_class: "app", media_content_id: MANUAL_MEDIA_SOURCE_PREFIX, media_content_type: "", iconPath: mdiKeyboard, title: "Manual entry", }; @customElement("ha-media-player-browse") export class HaMediaPlayerBrowse extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public entityId?: string; @property() public action: MediaPlayerBrowseAction = "play"; @property({ attribute: false }) public preferredLayout: MediaPlayerLayoutType = "auto"; @property({ type: Boolean }) public dialog = false; @property({ attribute: false }) public navigateIds: MediaPlayerItemId[] = []; @property({ attribute: false }) public accept?: string[]; @property({ attribute: false }) public defaultId?: string; @property({ attribute: false }) public defaultType?: string; @property({ attribute: false }) public hideContentType = false; @property({ attribute: false }) public contentIdHelper?: string; // @todo Consider reworking to eliminate need for attribute since it is manipulated internally @property({ type: Boolean, reflect: true }) public narrow = false; // @todo Consider reworking to eliminate need for attribute since it is manipulated internally @property({ type: Boolean, reflect: true }) public scrolled = false; @state() private _error?: { message: string; code: string }; @state() private _parentItem?: MediaPlayerItem; @state() private _currentItem?: MediaPlayerItem; @query(".header") private _header?: HTMLDivElement; @query(".content") private _content?: HTMLDivElement; @query("lit-virtualizer") private _virtualizer?: LitVirtualizer; private _observed = false; private _headerOffsetHeight = 0; private _resizeObserver?: ResizeObserver; public connectedCallback(): void { super.connectedCallback(); this.updateComplete.then(() => this._attachResizeObserver()); } public disconnectedCallback(): void { super.disconnectedCallback(); if (this._resizeObserver) { this._resizeObserver.disconnect(); } } public async refresh() { const currentId = this.navigateIds[this.navigateIds.length - 1]; try { this._currentItem = await this._fetchData( this.entityId, currentId.media_content_id, currentId.media_content_type ); // Update the parent with latest item. fireEvent(this, "media-browsed", { ids: this.navigateIds, current: this._currentItem, }); } catch (err) { this._setError(err); } } public play(): void { if (this._currentItem?.can_play) { this._runAction(this._currentItem); } } public willUpdate(changedProps: PropertyValues): void { super.willUpdate(changedProps); if (!this.hasUpdated) { loadVirtualizer(); } if (changedProps.has("entityId")) { this._setError(undefined); } else if (!changedProps.has("navigateIds")) { // Neither entity ID or navigateIDs changed, nothing to fetch return; } this._setError(undefined); const oldNavigateIds = changedProps.get("navigateIds") as | this["navigateIds"] | undefined; const navigateIds = this.navigateIds; // We're navigating. Reset the shizzle. this._content?.scrollTo(0, 0); this.scrolled = false; const oldCurrentItem = this._currentItem; const oldParentItem = this._parentItem; this._currentItem = undefined; this._parentItem = undefined; const currentId = navigateIds[navigateIds.length - 1]; const parentId = navigateIds.length > 1 ? navigateIds[navigateIds.length - 2] : undefined; let currentProm: Promise | undefined; let parentProm: Promise | undefined; // See if we can take loading shortcuts if navigating to parent or child if (!changedProps.has("entityId")) { if ( // Check if we navigated to a child oldNavigateIds && navigateIds.length === oldNavigateIds.length + 1 && oldNavigateIds.every((oldVal, idx) => { const curVal = navigateIds[idx]; return ( curVal.media_content_id === oldVal.media_content_id && curVal.media_content_type === oldVal.media_content_type ); }) ) { parentProm = Promise.resolve(oldCurrentItem!); } else if ( // Check if we navigated to a parent oldNavigateIds && navigateIds.length === oldNavigateIds.length - 1 && navigateIds.every((curVal, idx) => { const oldVal = oldNavigateIds[idx]; return ( curVal.media_content_id === oldVal.media_content_id && curVal.media_content_type === oldVal.media_content_type ); }) ) { currentProm = Promise.resolve(oldParentItem!); } } // Fetch current if ( currentId.media_content_id && isManualMediaSourceContentId(currentId.media_content_id) ) { this._currentItem = MANUAL_ITEM; fireEvent(this, "media-browsed", { ids: navigateIds, current: this._currentItem, }); } else { if (!currentProm) { currentProm = this._fetchData( this.entityId, currentId.media_content_id, currentId.media_content_type ); } currentProm.then( (item) => { this._currentItem = item; fireEvent(this, "media-browsed", { ids: navigateIds, current: item, }); }, (err) => { // When we change entity ID, we will first try to see if the new entity is // able to resolve the new path. If that results in an error, browse the root. const isNewEntityWithSamePath = oldNavigateIds && changedProps.has("entityId") && navigateIds.length === oldNavigateIds.length && oldNavigateIds.every( (oldItem, idx) => navigateIds[idx].media_content_id === oldItem.media_content_id && navigateIds[idx].media_content_type === oldItem.media_content_type ); if (isNewEntityWithSamePath) { fireEvent(this, "media-browsed", { ids: [ { media_content_id: undefined, media_content_type: undefined }, ], replace: true, }); } else if ( err.code === "entity_not_found" && this.entityId && isUnavailableState(this.hass.states[this.entityId]?.state) ) { this._setError({ message: this.hass.localize( `ui.components.media-browser.media_player_unavailable` ), code: "entity_not_found", }); } else { this._setError(err); } } ); } // Fetch parent if (!parentProm && parentId !== undefined) { parentProm = this._fetchData( this.entityId, parentId.media_content_id, parentId.media_content_type ); } if (parentProm) { parentProm.then((parent) => { this._parentItem = parent; }); } } 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); if (changedProps.has("_scrolled")) { this._animateHeaderHeight(); } else if (changedProps.has("_currentItem")) { this._setHeaderHeight(); // This fixes a race condition for resizing of the cards using the grid layout if (this._observed) { return; } // @ts-ignore const virtualizer = this._virtualizer?._virtualizer; if (virtualizer) { this._observed = true; setTimeout(() => virtualizer._observeMutations(), 0); } } } protected render() { 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}` ); let children = currentItem.children || []; const canPlayChildren = new Set(); // Filter children based on accept property if provided if (this.accept && children.length > 0) { let checks: ((t: string) => boolean)[] = []; for (const type of this.accept) { if (type.endsWith("/*")) { const baseType = type.slice(0, -1); checks.push((t) => t.startsWith(baseType)); } else if (type === "*") { checks = [() => true]; break; } else { checks.push((t) => t === type); } } children = children.filter((child) => { const contentType = child.media_content_type.toLowerCase(); const canPlay = child.media_content_type && checks.some((check) => check(contentType)); if (canPlay) { canPlayChildren.add(child.media_content_id); } return !child.media_content_type || child.can_expand || canPlay; }); } const mediaClass = MediaClassBrowserSettings[currentItem.media_class]; const childrenMediaClass = currentItem.children_media_class ? MediaClassBrowserSettings[currentItem.children_media_class] : MediaClassBrowserSettings.directory; const backgroundImage = currentItem.thumbnail ? this._getThumbnailURLorBase64(currentItem.thumbnail).then( (value) => `url(${value})` ) : "none"; return html` ${ currentItem.can_play ? html`
${currentItem.thumbnail ? html`
${this.narrow && currentItem?.can_play && (!this.accept || canPlayChildren.has( currentItem.media_content_id )) ? html` ${this.hass.localize( `ui.components.media-browser.${this.action}` )} ` : ""}
` : nothing}
${currentItem.can_play && (!currentItem.thumbnail || !this.narrow) ? html` ${this.hass.localize( `ui.components.media-browser.${this.action}` )} ` : ""}
` : "" }
${ this._error ? html`
${this._renderError(this._error)}
` : isManualMediaSourceContentId(currentItem.media_content_id) ? html`` : 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" )}
` : this.preferredLayout === "grid" || (this.preferredLayout === "auto" && 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._getThumbnailURLorBase64(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._getThumbnailURLorBase64(child.thumbnail).then( (value) => `url(${value})` ) : "none"; return html` ${backgroundImage === "none" && !child.can_play ? html`` : html`
${child.can_play ? html`` : nothing}
`} ${child.title}
`; }; private async _getThumbnailURLorBase64( thumbnailUrl: string | undefined ): Promise { if (!thumbnailUrl) { return ""; } if (thumbnailUrl.startsWith("/")) { // Thumbnails served by local API require authentication return new Promise((resolve, reject) => { this.hass .fetchWithAuth(thumbnailUrl!) // Since we are fetching with an authorization header, we cannot just put the // URL directly into the document; we need to embed the image. We could do this // using blob URLs, but then we would need to keep track of them in order to // release them properly. Instead, we embed the thumbnail using base64. .then((response) => response.blob()) .then((blob) => { const reader = new FileReader(); reader.onload = () => { const result = reader.result; resolve(typeof result === "string" ? result : ""); }; reader.onerror = (e) => reject(e); reader.readAsDataURL(blob); }); }); } if (isBrandUrl(thumbnailUrl)) { // 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 }); } private _ttsPicked(ev: CustomEvent): void { ev.stopPropagation(); const navigateIds = this.navigateIds.slice(0, -1); navigateIds.push(ev.detail.item); fireEvent(this, "media-picked", { ...ev.detail, navigateIds, }); } private _manualPicked(ev: CustomEvent) { ev.stopPropagation(); fireEvent(this, "media-picked", { item: ev.detail.item as MediaPlayerItem, navigateIds: this.navigateIds, }); } private _childClicked = async (ev: MouseEvent): Promise => { const target = ev.currentTarget as any; const item: MediaPlayerItem = target.item; if (!item) { return; } if (!item.can_expand) { this._runAction(item); return; } fireEvent(this, "media-browsed", { ids: [...this.navigateIds, item], }); }; private async _fetchData( entityId: string | undefined, mediaContentId?: string, mediaContentType?: string ): Promise { const prom = entityId && entityId !== BROWSER_PLAYER ? browseMediaPlayer( this.hass, entityId, mediaContentId, mediaContentType ) : browseLocalMediaPlayer(this.hass, mediaContentId); return prom.then((item) => { if (!mediaContentId && this.action === "pick") { item.children = item.children || []; item.children.push(MANUAL_ITEM); } return item; }); } private _measureCard(): void { this.narrow = (this.dialog ? window.innerWidth : this.offsetWidth) < 450; } private async _attachResizeObserver(): Promise { if (!this._resizeObserver) { this._resizeObserver = new ResizeObserver( debounce(() => this._measureCard(), 250, false) ); } this._resizeObserver.observe(this); } private _closeDialogAction(): void { fireEvent(this, "close-dialog"); } private _setError(error: any) { if (!this.dialog) { this._error = error; return; } if (!error) { return; } this._closeDialogAction(); showAlertDialog(this, { title: this.hass.localize( "ui.components.media-browser.media_browsing_error" ), text: this._renderError(error), }); } private _renderError(err: { message: string; code: string }) { if (err.message === "Media directory does not exist.") { return html`

${this.hass.localize( "ui.components.media-browser.no_local_media_found" )}

${this.hass.localize("ui.components.media-browser.no_media_folder")}
${this.hass.localize("ui.components.media-browser.setup_local_help", { documentation: html`${this.hass.localize( "ui.components.media-browser.documentation" )}`, })}
${this.hass.localize("ui.components.media-browser.local_media_files")}

`; } 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, css` :host { display: flex; flex-direction: column; position: relative; direction: ltr; } ha-spinner { margin: 40px auto; } .container { padding: 16px; } .no-items { padding-left: 32px; } .highlight-add-button { display: flex; flex-direction: row-reverse; margin-right: 48px; margin-inline-end: 48px; margin-inline-start: initial; direction: var(--direction); } .highlight-add-button ha-svg-icon { position: relative; top: -0.5em; margin-left: 8px; margin-inline-start: 8px; margin-inline-end: initial; transform: scaleX(var(--scale-direction)); } .content { overflow-y: auto; box-sizing: border-box; height: 100%; } /* HEADER */ .header { display: flex; justify-content: space-between; border-bottom: 1px solid var(--divider-color); background-color: var(--card-background-color); position: absolute; top: 0; right: 0; left: 0; z-index: 3; 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: 175px; width: 175px; margin-right: 16px; background-size: cover; border-radius: 2px; transition: width 0.4s, height 0.4s; } .header-info { display: flex; flex-direction: column; justify-content: space-between; align-self: stretch; min-width: 0; flex: 1; } .header-info ha-button { display: block; padding-bottom: 16px; } .breadcrumb { display: flex; flex-direction: column; overflow: hidden; flex-grow: 1; padding-top: 16px; } .breadcrumb .title { font-size: var(--ha-font-size-4xl); line-height: var(--ha-line-height-condensed); font-weight: var(--ha-font-weight-bold); margin: 0; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; padding-right: 8px; } .breadcrumb .previous-title { font-size: var(--ha-font-size-m); padding-bottom: 8px; color: var(--secondary-text-color); overflow: hidden; text-overflow: ellipsis; cursor: pointer; --mdc-icon-size: 14px; } .breadcrumb .subtitle { font-size: var(--ha-font-size-l); overflow: hidden; text-overflow: ellipsis; margin-bottom: 0; transition: height 0.5s, margin 0.5s; } .not-shown { font-style: italic; color: var(--secondary-text-color); padding: 8px 16px 8px; } .grid.not-shown { display: flex; align-items: center; text-align: center; } /* ============= CHILDREN ============= */ ha-list { --mdc-list-vertical-padding: 0; --mdc-list-item-graphic-margin: 0; --mdc-theme-text-icon-on-background: var(--secondary-text-color); margin-top: 10px; } ha-list li:last-child { display: none; } ha-list li[divider] { border-bottom-color: var(--divider-color); } ha-list-item { width: 100%; } div.children { display: grid; grid-template-columns: repeat( auto-fit, minmax(var(--media-browse-item-size, 175px), 0.1fr) ); grid-gap: var(--ha-space-4); padding: 16px; } :host([dialog]) .children { grid-template-columns: repeat( auto-fit, minmax(var(--media-browse-item-size, 175px), 0.33fr) ); } .child { display: flex; flex-direction: column; cursor: pointer; } ha-card { position: relative; width: 100%; box-sizing: border-box; } .children ha-card .thumbnail { width: 100%; position: relative; box-sizing: border-box; transition: padding-bottom 0.1s ease-out; padding-bottom: 100%; } .portrait ha-card .thumbnail { padding-bottom: 150%; } ha-card .image { border-radius: var(--ha-border-radius-sm) var(--ha-border-radius-sm) var(--ha-border-radius-square) var(--ha-border-radius-square); } .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; } .brand-image { background-size: 40%; } .children ha-card .icon-holder { display: flex; justify-content: center; align-items: center; } .child .folder { color: var(--secondary-text-color); --mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4); } .child .icon { color: #00a9f7; /* Match the png color from brands repo */ --mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4); } .child .play { position: absolute; transition: color 0.5s; border-radius: var(--ha-border-radius-circle); top: calc(50% - 40px); 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; background-color: var(--primary-color); color: var(--text-primary-color); } ha-card:hover .image { filter: brightness(70%); transition: filter 0.5s; } ha-card:hover .play { opacity: 1; } ha-card:hover .play.can_expand { bottom: 8px; } .child .play.can_expand { background-color: rgba(var(--rgb-card-background-color), 0.5); top: auto; bottom: 0px; right: 8px; transition: bottom 0.1s ease-out, opacity 0.1s ease-out; } .child .title { font-size: var(--ha-font-size-l); padding-top: 16px; padding-left: 2px; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; text-overflow: ellipsis; } .child ha-card .title { margin-bottom: 16px; padding-left: 16px; } ha-list-item .graphic { background-size: contain; background-repeat: no-repeat; background-position: center; border-radius: var(--ha-border-radius-sm); display: flex; align-content: center; align-items: center; line-height: initial; } ha-list-item .graphic .play { opacity: 0; transition: all 0.5s; background-color: rgba(var(--rgb-card-background-color), 0.5); border-radius: var(--ha-border-radius-circle); --mdc-icon-button-size: 40px; } ha-list-item:hover .graphic .play { opacity: 1; color: var(--primary-text-color); } ha-list-item .graphic .play.show { opacity: 1; background-color: transparent; } ha-list-item .title { margin-left: 16px; margin-inline-start: 16px; margin-inline-end: initial; } /* ============= Narrow ============= */ :host([narrow]) { padding: 0; } :host([narrow]) .media-source { padding: 0 24px; } :host([narrow]) div.children { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important; } :host([narrow]) .breadcrumb .title { font-size: var(--ha-font-size-2xl); } :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%; margin-right: 0; padding-bottom: 50%; margin-bottom: 8px; position: relative; background-position: center; border-radius: var(--ha-border-radius-square); 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 ha-button { margin-top: 16px; margin-bottom: 8px; } :host([narrow]) .header-info { padding: 0 16px 8px; } /* ============= Scroll ============= */ :host([scrolled]) .breadcrumb .subtitle { height: 0; margin: 0; } :host([scrolled]) .breadcrumb .title { -webkit-line-clamp: 1; } :host(:not([narrow])[scrolled]) .header:not(.no-img) ha-icon-button { align-self: center; } :host([scrolled]) .header-info ha-button, .no-img .header-info ha-button { padding-right: 4px; } :host([scrolled][narrow]) .no-img .header-info ha-button { padding-right: 16px; } :host([scrolled]) .header-info { flex-direction: row; } :host([scrolled]) .header-info ha-button { align-self: center; margin-top: 0; margin-bottom: 0; padding-bottom: 0; } :host([scrolled][narrow]) .no-img .header-info { flex-direction: row-reverse; } :host([scrolled][narrow]) .header-info { padding: 20px 24px 10px 24px; align-items: center; } :host([scrolled]) .header-content { align-items: flex-end; flex-direction: row; } :host([scrolled]) .header-content .img { height: 75px; width: 75px; } :host([scrolled]) .breadcrumb { padding-top: 0; align-self: center; } :host([scrolled][narrow]) .header-content .img { height: 100px; width: 100px; padding-bottom: initial; margin-bottom: 0; } :host([scrolled]) ha-fab { bottom: 0px; right: -24px; --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); } ha-browse-media-tts { direction: var(--direction); } `, ]; } } declare global { interface HTMLElementTagNameMap { "ha-media-player-browse": HaMediaPlayerBrowse; } }