mirror of
https://github.com/home-assistant/frontend.git
synced 2025-10-20 17:20:07 +00:00
433 lines
14 KiB
TypeScript
433 lines
14 KiB
TypeScript
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<this>) {
|
|
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`<label>${this.label}</label>` : nothing}
|
|
<ha-picture-upload
|
|
.hass=${this.hass}
|
|
.value=${null}
|
|
.contentIdHelper=${this.selector.media?.content_id_helper}
|
|
select-media
|
|
full-media
|
|
@media-picked=${this._pictureUploadMediaPicked}
|
|
></ha-picture-upload>`;
|
|
}
|
|
|
|
return html`
|
|
${this._hasAccept ||
|
|
(this._contextEntities && this._contextEntities.length <= 1)
|
|
? nothing
|
|
: html`
|
|
<ha-entity-picker
|
|
.hass=${this.hass}
|
|
.value=${entityId}
|
|
.label=${this.label ||
|
|
this.hass.localize(
|
|
"ui.components.selectors.media.pick_media_player"
|
|
)}
|
|
.disabled=${this.disabled}
|
|
.helper=${this.helper}
|
|
.required=${this.required}
|
|
.hideClearIcon=${!!this._contextEntities}
|
|
.includeDomains=${INCLUDE_DOMAINS}
|
|
.includeEntities=${this._contextEntities}
|
|
.allowCustomEntity=${!this._contextEntities}
|
|
@value-changed=${this._entityChanged}
|
|
></ha-entity-picker>
|
|
`}
|
|
${!supportsBrowse
|
|
? html`
|
|
${this.label ? html`<label>${this.label}</label>` : nothing}
|
|
<ha-alert>
|
|
${this.hass.localize(
|
|
"ui.components.selectors.media.browse_not_supported"
|
|
)}
|
|
</ha-alert>
|
|
<ha-form
|
|
.hass=${this.hass}
|
|
.data=${this.value || EMPTY_FORM}
|
|
.schema=${MANUAL_SCHEMA}
|
|
.computeLabel=${this._computeLabelCallback}
|
|
.computeHelper=${this._computeHelperCallback}
|
|
></ha-form>
|
|
`
|
|
: html`${this.label ? html`<label>${this.label}</label>` : nothing}
|
|
<ha-card
|
|
outlined
|
|
tabindex="0"
|
|
role="button"
|
|
aria-label=${!this.value?.media_content_id
|
|
? this.hass.localize("ui.components.selectors.media.pick_media")
|
|
: this.value.metadata?.title || this.value.media_content_id}
|
|
@click=${this._pickMedia}
|
|
@keydown=${this._handleKeyDown}
|
|
class=${this.disabled || (!entityId && !this._hasAccept)
|
|
? "disabled"
|
|
: ""}
|
|
>
|
|
<div class="content-container">
|
|
<div class="thumbnail">
|
|
${this.value?.metadata?.thumbnail
|
|
? html`
|
|
<div
|
|
class="${classMap({
|
|
"centered-image":
|
|
!!this.value.metadata.media_class &&
|
|
["app", "directory"].includes(
|
|
this.value.metadata.media_class
|
|
),
|
|
})}
|
|
image"
|
|
style=${this._thumbnailUrl
|
|
? `background-image: url(${this._thumbnailUrl});`
|
|
: ""}
|
|
></div>
|
|
`
|
|
: html`
|
|
<div class="icon-holder image">
|
|
<ha-svg-icon
|
|
class="folder"
|
|
.path=${!this.value?.media_content_id
|
|
? mdiPlus
|
|
: this.value?.metadata?.media_class
|
|
? MediaClassBrowserSettings[
|
|
this.value.metadata.media_class ===
|
|
"directory"
|
|
? this.value.metadata
|
|
.children_media_class ||
|
|
this.value.metadata.media_class
|
|
: this.value.metadata.media_class
|
|
].icon
|
|
: mdiPlayBox}
|
|
></ha-svg-icon>
|
|
</div>
|
|
`}
|
|
</div>
|
|
<div class="title">
|
|
${!this.value?.media_content_id
|
|
? this.hass.localize(
|
|
"ui.components.selectors.media.pick_media"
|
|
)
|
|
: this.value.metadata?.title || this.value.media_content_id}
|
|
</div>
|
|
</div>
|
|
</ha-card>
|
|
${this.selector.media?.clearable
|
|
? html`<div>
|
|
<ha-button
|
|
appearance="plain"
|
|
size="small"
|
|
variant="danger"
|
|
@click=${this._clearValue}
|
|
>
|
|
${this.hass.localize(
|
|
"ui.components.picture-upload.clear_picture"
|
|
)}
|
|
</ha-button>
|
|
</div>`
|
|
: nothing}`}
|
|
`;
|
|
}
|
|
|
|
private _computeLabelCallback = (
|
|
schema: SchemaUnion<typeof MANUAL_SCHEMA>
|
|
): string =>
|
|
this.hass.localize(`ui.components.selectors.media.${schema.name}`);
|
|
|
|
private _computeHelperCallback = (
|
|
schema: SchemaUnion<typeof MANUAL_SCHEMA>
|
|
): 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;
|
|
}
|
|
}
|