import { mdiPlayBox, mdiPlus } from "@mdi/js"; import type { PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../common/dom/fire_event"; import { supportsFeature } from "../../common/entity/supports-feature"; import { getSignedPath } from "../../data/auth"; import type { MediaPickedEvent } from "../../data/media-player"; import { MediaClassBrowserSettings, MediaPlayerEntityFeature, } from "../../data/media-player"; import type { MediaSelector, MediaSelectorValue } from "../../data/selector"; import type { HomeAssistant } from "../../types"; import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url"; import "../ha-alert"; import "../ha-form/ha-form"; import type { SchemaUnion } from "../ha-form/types"; import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog"; import { ensureArray } from "../../common/array/ensure-array"; import "../ha-picture-upload"; const MANUAL_SCHEMA = [ { name: "media_content_id", required: false, selector: { text: {} } }, { name: "media_content_type", required: false, selector: { text: {} } }, ] as const; const INCLUDE_DOMAINS = ["media_player"]; const EMPTY_FORM = {}; @customElement("ha-selector-media") export class HaMediaSelector extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public selector!: MediaSelector; @property({ attribute: false }) public value?: MediaSelectorValue; @property() public label?: string; @property() public helper?: string; @property({ type: Boolean, reflect: true }) public disabled = false; @property({ type: Boolean, reflect: true }) public required = true; @property({ attribute: false }) public context?: { filter_entity?: string | string[]; }; @state() private _thumbnailUrl?: string | null; private _contextEntities: string[] | undefined; private get _hasAccept(): boolean { return !!this.selector?.media?.accept?.length; } willUpdate(changedProps: PropertyValues) { if (changedProps.has("context")) { if (!this._hasAccept) { this._contextEntities = ensureArray(this.context?.filter_entity); } } if (changedProps.has("value")) { const thumbnail = this.value?.metadata?.thumbnail; const oldThumbnail = (changedProps.get("value") as this["value"]) ?.metadata?.thumbnail; if (thumbnail === oldThumbnail) { return; } if (thumbnail && thumbnail.startsWith("/")) { this._thumbnailUrl = undefined; // Thumbnails served by local API require authentication getSignedPath(this.hass, thumbnail).then((signedPath) => { this._thumbnailUrl = signedPath.path; }); } else if ( thumbnail && thumbnail.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 this._thumbnailUrl = brandsUrl({ domain: extractDomainFromBrandUrl(thumbnail), type: "icon", useFallback: true, darkOptimized: this.hass.themes?.darkMode, }); } else { this._thumbnailUrl = thumbnail; } } } protected render() { const entityId = this._getActiveEntityId(); const stateObj = entityId ? this.hass.states[entityId] : undefined; const supportsBrowse = !entityId || (stateObj && supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)); if (this.selector.media?.image_upload && !this.value) { return html`${this.label ? html`` : nothing} `; } return html` ${this._hasAccept || (this._contextEntities && this._contextEntities.length <= 1) ? nothing : html` `} ${!supportsBrowse ? html` ${this.label ? html`` : nothing} ${this.hass.localize( "ui.components.selectors.media.browse_not_supported" )} ` : html`${this.label ? html`` : nothing}
${this.value?.metadata?.thumbnail ? html`
` : html`
`}
${!this.value?.media_content_id ? this.hass.localize( "ui.components.selectors.media.pick_media" ) : this.value.metadata?.title || this.value.media_content_id}
${this.selector.media?.clearable ? html`
${this.hass.localize( "ui.components.picture-upload.clear_picture" )}
` : nothing}`} `; } private _computeLabelCallback = ( schema: SchemaUnion ): string => this.hass.localize(`ui.components.selectors.media.${schema.name}`); private _computeHelperCallback = ( schema: SchemaUnion ): string => this.hass.localize(`ui.components.selectors.media.${schema.name}_detail`); private _entityChanged(ev: CustomEvent) { ev.stopPropagation(); if (!this._hasAccept && this.context?.filter_entity) { fireEvent(this, "value-changed", { value: { media_content_id: "", media_content_type: "", metadata: { browse_entity_id: ev.detail.value, }, }, }); } else { fireEvent(this, "value-changed", { value: { entity_id: ev.detail.value, media_content_id: "", media_content_type: "", }, }); } } private _pickMedia() { showMediaBrowserDialog(this, { action: "pick", entityId: this._getActiveEntityId(), navigateIds: this.value?.metadata?.navigateIds, accept: this.selector.media?.accept, defaultId: this.value?.media_content_id, defaultType: this.value?.media_content_type, hideContentType: this.selector.media?.hide_content_type, contentIdHelper: this.selector.media?.content_id_helper, mediaPickedCallback: (pickedMedia: MediaPickedEvent) => { fireEvent(this, "value-changed", { value: { ...this.value, media_content_id: pickedMedia.item.media_content_id, media_content_type: pickedMedia.item.media_content_type, metadata: { title: pickedMedia.item.title, thumbnail: pickedMedia.item.thumbnail, media_class: pickedMedia.item.media_class, children_media_class: pickedMedia.item.children_media_class, navigateIds: pickedMedia.navigateIds?.map((id) => ({ media_content_type: id.media_content_type, media_content_id: id.media_content_id, })), ...(!this._hasAccept && this.context?.filter_entity ? { browse_entity_id: this._getActiveEntityId() } : {}), }, }, }); }, }); } private _getActiveEntityId(): string | undefined { const metaId = this.value?.metadata?.browse_entity_id; return ( this.value?.entity_id || (metaId && this._contextEntities?.includes(metaId) && metaId) || this._contextEntities?.[0] ); } private _handleKeyDown(ev: KeyboardEvent) { if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); this._pickMedia(); } } private _pictureUploadMediaPicked(ev) { const pickedMedia = ev.detail as MediaPickedEvent; fireEvent(this, "value-changed", { value: { ...this.value, media_content_id: pickedMedia.item.media_content_id, media_content_type: pickedMedia.item.media_content_type, metadata: { title: pickedMedia.item.title, thumbnail: pickedMedia.item.thumbnail, media_class: pickedMedia.item.media_class, children_media_class: pickedMedia.item.children_media_class, navigateIds: pickedMedia.navigateIds?.map((id) => ({ media_content_type: id.media_content_type, media_content_id: id.media_content_id, })), }, }, }); } private _clearValue() { fireEvent(this, "value-changed", { value: undefined }); } static styles = css` ha-entity-picker { display: block; margin-bottom: 16px; } ha-alert { display: block; margin-bottom: 16px; } ha-card { position: relative; width: 100%; box-sizing: border-box; cursor: pointer; transition: background-color 180ms ease-in-out; min-height: 56px; } ha-card:hover:not(.disabled), ha-card:focus:not(.disabled) { background-color: var(--state-icon-hover-color, rgba(0, 0, 0, 0.04)); } ha-card:focus { outline: none; } ha-card.disabled { pointer-events: none; color: var(--disabled-text-color); } .content-container { display: flex; align-items: center; padding: 8px; gap: var(--ha-space-3); } ha-card .thumbnail { width: 40px; height: 40px; flex-shrink: 0; position: relative; box-sizing: border-box; border-radius: var(--ha-border-radius-md); overflow: hidden; } ha-card .image { border-radius: var(--ha-border-radius-md); } .folder { --mdc-icon-size: 24px; } .title { font-size: var(--ha-font-size-m); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.4; flex: 1; min-width: 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: 4px; background-size: contain; } .icon-holder { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; } `; } declare global { interface HTMLElementTagNameMap { "ha-selector-media": HaMediaSelector; } }