289 lines
9.3 KiB
TypeScript
289 lines
9.3 KiB
TypeScript
import { mdiPlayBox, mdiPlus } from "@mdi/js";
|
|
import { css, CSSResultGroup, html, LitElement, PropertyValues } 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 {
|
|
MediaClassBrowserSettings,
|
|
MediaPickedEvent,
|
|
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";
|
|
|
|
const MANUAL_SCHEMA = [
|
|
{ name: "media_content_id", required: false, selector: { text: {} } },
|
|
{ name: "media_content_type", required: false, selector: { text: {} } },
|
|
] as const;
|
|
|
|
@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;
|
|
|
|
@state() private _thumbnailUrl?: string | null;
|
|
|
|
willUpdate(changedProps: PropertyValues<this>) {
|
|
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 stateObj = this.value?.entity_id
|
|
? this.hass.states[this.value.entity_id]
|
|
: undefined;
|
|
|
|
const supportsBrowse =
|
|
!this.value?.entity_id ||
|
|
(stateObj &&
|
|
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
|
|
|
|
return html`<ha-entity-picker
|
|
.hass=${this.hass}
|
|
.value=${this.value?.entity_id}
|
|
.label=${this.label ||
|
|
this.hass.localize("ui.components.selectors.media.pick_media_player")}
|
|
.disabled=${this.disabled}
|
|
.helper=${this.helper}
|
|
.required=${this.required}
|
|
include-domains='["media_player"]'
|
|
allow-custom-entity
|
|
@value-changed=${this._entityChanged}
|
|
></ha-entity-picker>
|
|
${!supportsBrowse
|
|
? html`<ha-alert>
|
|
${this.hass.localize(
|
|
"ui.components.selectors.media.browse_not_supported"
|
|
)}
|
|
</ha-alert>
|
|
<ha-form
|
|
.hass=${this.hass}
|
|
.data=${this.value}
|
|
.schema=${MANUAL_SCHEMA}
|
|
.computeLabel=${this._computeLabelCallback}
|
|
></ha-form>`
|
|
: html`<ha-card
|
|
outlined
|
|
@click=${this._pickMedia}
|
|
class=${this.disabled || !this.value?.entity_id ? "disabled" : ""}
|
|
>
|
|
<div
|
|
class="thumbnail ${classMap({
|
|
portrait:
|
|
!!this.value?.metadata?.media_class &&
|
|
MediaClassBrowserSettings[
|
|
this.value.metadata.children_media_class ||
|
|
this.value.metadata.media_class
|
|
].thumbnail_ratio === "portrait",
|
|
})}"
|
|
>
|
|
${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>
|
|
</ha-card>`}`;
|
|
}
|
|
|
|
private _computeLabelCallback = (
|
|
schema: SchemaUnion<typeof MANUAL_SCHEMA>
|
|
): string =>
|
|
this.hass.localize(`ui.components.selectors.media.${schema.name}`);
|
|
|
|
private _entityChanged(ev: CustomEvent) {
|
|
ev.stopPropagation();
|
|
fireEvent(this, "value-changed", {
|
|
value: {
|
|
entity_id: ev.detail.value,
|
|
media_content_id: "",
|
|
media_content_type: "",
|
|
},
|
|
});
|
|
}
|
|
|
|
private _pickMedia() {
|
|
showMediaBrowserDialog(this, {
|
|
action: "pick",
|
|
entityId: this.value!.entity_id!,
|
|
navigateIds: this.value!.metadata?.navigateIds,
|
|
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,
|
|
})),
|
|
},
|
|
},
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
static get styles(): CSSResultGroup {
|
|
return css`
|
|
ha-entity-picker {
|
|
display: block;
|
|
margin-bottom: 16px;
|
|
}
|
|
mwc-button {
|
|
margin-top: 8px;
|
|
}
|
|
ha-alert {
|
|
display: block;
|
|
margin-bottom: 16px;
|
|
}
|
|
ha-card {
|
|
position: relative;
|
|
width: 200px;
|
|
box-sizing: border-box;
|
|
cursor: pointer;
|
|
}
|
|
ha-card.disabled {
|
|
pointer-events: none;
|
|
color: var(--disabled-text-color);
|
|
}
|
|
ha-card .thumbnail {
|
|
width: 100%;
|
|
position: relative;
|
|
box-sizing: border-box;
|
|
transition: padding-bottom 0.1s ease-out;
|
|
padding-bottom: 100%;
|
|
}
|
|
ha-card .thumbnail.portrait {
|
|
padding-bottom: 150%;
|
|
}
|
|
ha-card .image {
|
|
border-radius: 3px 3px 0 0;
|
|
}
|
|
.folder {
|
|
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
|
}
|
|
.title {
|
|
font-size: 16px;
|
|
padding-top: 16px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
margin-bottom: 16px;
|
|
padding-left: 16px;
|
|
padding-right: 4px;
|
|
padding-inline-start: 16px;
|
|
padding-inline-end: 4px;
|
|
white-space: nowrap;
|
|
}
|
|
.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;
|
|
}
|
|
.icon-holder {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
`;
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"ha-selector-media": HaMediaSelector;
|
|
}
|
|
}
|