Add support for accept keyword in media selector (#25808)

This commit is contained in:
Paulus Schoutsen 2025-06-22 14:02:39 -04:00 committed by GitHub
parent fdd6ccf379
commit 589fa75b17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 150 additions and 78 deletions

View File

@ -1,6 +1,6 @@
import { mdiPlayBox, mdiPlus } from "@mdi/js"; import { mdiPlayBox, mdiPlus } from "@mdi/js";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
@ -84,20 +84,30 @@ export class HaMediaSelector extends LitElement {
(stateObj && (stateObj &&
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)); supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
return html`<ha-entity-picker const hasAccept = this.selector.media?.accept?.length;
.hass=${this.hass}
.value=${this.value?.entity_id} return html`
.label=${this.label || ${hasAccept
this.hass.localize("ui.components.selectors.media.pick_media_player")} ? nothing
.disabled=${this.disabled} : html`
.helper=${this.helper} <ha-entity-picker
.required=${this.required} .hass=${this.hass}
include-domains='["media_player"]' .value=${this.value?.entity_id}
allow-custom-entity .label=${this.label ||
@value-changed=${this._entityChanged} this.hass.localize(
></ha-entity-picker> "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 ${!supportsBrowse
? html`<ha-alert> ? html`
<ha-alert>
${this.hass.localize( ${this.hass.localize(
"ui.components.selectors.media.browse_not_supported" "ui.components.selectors.media.browse_not_supported"
)} )}
@ -107,62 +117,72 @@ export class HaMediaSelector extends LitElement {
.data=${this.value} .data=${this.value}
.schema=${MANUAL_SCHEMA} .schema=${MANUAL_SCHEMA}
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
></ha-form>` ></ha-form>
: html`<ha-card `
outlined : html`
@click=${this._pickMedia} <ha-card
class=${this.disabled || !this.value?.entity_id ? "disabled" : ""} outlined
> @click=${this._pickMedia}
<div class=${this.disabled || (!this.value?.entity_id && !hasAccept)
class="thumbnail ${classMap({ ? "disabled"
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 <div
? html` class="thumbnail ${classMap({
<div portrait:
class="${classMap({ !!this.value?.metadata?.media_class &&
"centered-image": MediaClassBrowserSettings[
!!this.value.metadata.media_class && this.value.metadata.children_media_class ||
["app", "directory"].includes( this.value.metadata.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" image"
style=${this._thumbnailUrl style=${this._thumbnailUrl
? `background-image: url(${this._thumbnailUrl});` ? `background-image: url(${this._thumbnailUrl});`
: ""} : ""}
></div> ></div>
` `
: html` : html`
<div class="icon-holder image"> <div class="icon-holder image">
<ha-svg-icon <ha-svg-icon
class="folder" class="folder"
.path=${!this.value?.media_content_id .path=${!this.value?.media_content_id
? mdiPlus ? mdiPlus
: this.value?.metadata?.media_class : this.value?.metadata?.media_class
? MediaClassBrowserSettings[ ? MediaClassBrowserSettings[
this.value.metadata.media_class === "directory" this.value.metadata.media_class ===
? this.value.metadata.children_media_class || "directory"
this.value.metadata.media_class ? this.value.metadata
: this.value.metadata.media_class .children_media_class ||
].icon this.value.metadata.media_class
: mdiPlayBox} : this.value.metadata.media_class
></ha-svg-icon> ].icon
</div> : mdiPlayBox}
`} ></ha-svg-icon>
</div> </div>
<div class="title"> `}
${!this.value?.media_content_id </div>
? this.hass.localize("ui.components.selectors.media.pick_media") <div class="title">
: this.value.metadata?.title || this.value.media_content_id} ${!this.value?.media_content_id
</div> ? this.hass.localize(
</ha-card>`}`; "ui.components.selectors.media.pick_media"
)
: this.value.metadata?.title || this.value.media_content_id}
</div>
</ha-card>
`}
`;
} }
private _computeLabelCallback = ( private _computeLabelCallback = (
@ -184,8 +204,9 @@ export class HaMediaSelector extends LitElement {
private _pickMedia() { private _pickMedia() {
showMediaBrowserDialog(this, { showMediaBrowserDialog(this, {
action: "pick", action: "pick",
entityId: this.value!.entity_id!, entityId: this.value?.entity_id,
navigateIds: this.value!.metadata?.navigateIds, navigateIds: this.value?.metadata?.navigateIds,
accept: this.selector.media?.accept,
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => { mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { value: {

View File

@ -80,7 +80,16 @@ const SELECTOR_SCHEMAS = {
] as const, ] as const,
icon: [] as const, icon: [] as const,
location: [] as const, location: [] as const,
media: [] as const, media: [
{
name: "accept",
selector: {
text: {
multiple: true,
},
},
},
] as const,
number: [ number: [
{ {
name: "min", name: "min",

View File

@ -164,6 +164,7 @@ class DialogMediaPlayerBrowse extends LitElement {
.navigateIds=${this._navigateIds} .navigateIds=${this._navigateIds}
.action=${this._action} .action=${this._action}
.preferredLayout=${this._preferredLayout} .preferredLayout=${this._preferredLayout}
.accept=${this._params.accept}
@close-dialog=${this.closeDialog} @close-dialog=${this.closeDialog}
@media-picked=${this._mediaPicked} @media-picked=${this._mediaPicked}
@media-browsed=${this._mediaBrowsed} @media-browsed=${this._mediaBrowsed}

View File

@ -78,7 +78,7 @@ export interface MediaPlayerItemId {
export class HaMediaPlayerBrowse extends LitElement { export class HaMediaPlayerBrowse extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId!: string; @property({ attribute: false }) public entityId?: string;
@property() public action: MediaPlayerBrowseAction = "play"; @property() public action: MediaPlayerBrowseAction = "play";
@ -89,6 +89,8 @@ export class HaMediaPlayerBrowse extends LitElement {
@property({ attribute: false }) public navigateIds: MediaPlayerItemId[] = []; @property({ attribute: false }) public navigateIds: MediaPlayerItemId[] = [];
@property({ attribute: false }) public accept?: string[];
// @todo Consider reworking to eliminate need for attribute since it is manipulated internally // @todo Consider reworking to eliminate need for attribute since it is manipulated internally
@property({ type: Boolean, reflect: true }) public narrow = false; @property({ type: Boolean, reflect: true }) public narrow = false;
@ -250,6 +252,7 @@ export class HaMediaPlayerBrowse extends LitElement {
}); });
} else if ( } else if (
err.code === "entity_not_found" && err.code === "entity_not_found" &&
this.entityId &&
isUnavailableState(this.hass.states[this.entityId]?.state) isUnavailableState(this.hass.states[this.entityId]?.state)
) { ) {
this._setError({ this._setError({
@ -334,7 +337,37 @@ export class HaMediaPlayerBrowse extends LitElement {
const subtitle = this.hass.localize( const subtitle = this.hass.localize(
`ui.components.media-browser.class.${currentItem.media_class}` `ui.components.media-browser.class.${currentItem.media_class}`
); );
const children = currentItem.children || []; let children = currentItem.children || [];
const canPlayChildren = new Set<string>();
// 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 mediaClass = MediaClassBrowserSettings[currentItem.media_class];
const childrenMediaClass = currentItem.children_media_class const childrenMediaClass = currentItem.children_media_class
? MediaClassBrowserSettings[currentItem.children_media_class] ? MediaClassBrowserSettings[currentItem.children_media_class]
@ -367,7 +400,12 @@ export class HaMediaPlayerBrowse extends LitElement {
"" ""
)}" )}"
> >
${this.narrow && currentItem?.can_play ${this.narrow &&
currentItem?.can_play &&
(!this.accept ||
canPlayChildren.has(
currentItem.media_content_id
))
? html` ? html`
<ha-fab <ha-fab
mini mini
@ -748,11 +786,11 @@ export class HaMediaPlayerBrowse extends LitElement {
}; };
private async _fetchData( private async _fetchData(
entityId: string, entityId: string | undefined,
mediaContentId?: string, mediaContentId?: string,
mediaContentType?: string mediaContentType?: string
): Promise<MediaPlayerItem> { ): Promise<MediaPlayerItem> {
return entityId !== BROWSER_PLAYER return entityId && entityId !== BROWSER_PLAYER
? browseMediaPlayer(this.hass, entityId, mediaContentId, mediaContentType) ? browseMediaPlayer(this.hass, entityId, mediaContentId, mediaContentType)
: browseLocalMediaPlayer(this.hass, mediaContentId); : browseLocalMediaPlayer(this.hass, mediaContentId);
} }

View File

@ -7,10 +7,11 @@ import type { MediaPlayerItemId } from "./ha-media-player-browse";
export interface MediaPlayerBrowseDialogParams { export interface MediaPlayerBrowseDialogParams {
action: MediaPlayerBrowseAction; action: MediaPlayerBrowseAction;
entityId: string; entityId?: string;
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void; mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void;
navigateIds?: MediaPlayerItemId[]; navigateIds?: MediaPlayerItemId[];
minimumNavigateLevel?: number; minimumNavigateLevel?: number;
accept?: string[];
} }
export const showMediaBrowserDialog = ( export const showMediaBrowserDialog = (

View File

@ -303,7 +303,9 @@ export interface LocationSelectorValue {
} }
export interface MediaSelector { export interface MediaSelector {
media: {} | null; media: {
accept?: string[];
} | null;
} }
export interface MediaSelectorValue { export interface MediaSelectorValue {