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 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 { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
@ -84,20 +84,30 @@ export class HaMediaSelector extends LitElement {
(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>
const hasAccept = this.selector.media?.accept?.length;
return html`
${hasAccept
? nothing
: 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>
? html`
<ha-alert>
${this.hass.localize(
"ui.components.selectors.media.browse_not_supported"
)}
@ -107,62 +117,72 @@ export class HaMediaSelector extends LitElement {
.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",
})}"
></ha-form>
`
: html`
<ha-card
outlined
@click=${this._pickMedia}
class=${this.disabled || (!this.value?.entity_id && !hasAccept)
? "disabled"
: ""}
>
${this.value?.metadata?.thumbnail
? html`
<div
class="${classMap({
"centered-image":
!!this.value.metadata.media_class &&
["app", "directory"].includes(
this.value.metadata.media_class
),
})}
<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>`}`;
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 = (
@ -184,8 +204,9 @@ export class HaMediaSelector extends LitElement {
private _pickMedia() {
showMediaBrowserDialog(this, {
action: "pick",
entityId: this.value!.entity_id!,
navigateIds: this.value!.metadata?.navigateIds,
entityId: this.value?.entity_id,
navigateIds: this.value?.metadata?.navigateIds,
accept: this.selector.media?.accept,
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
fireEvent(this, "value-changed", {
value: {

View File

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

View File

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

View File

@ -78,7 +78,7 @@ export interface MediaPlayerItemId {
export class HaMediaPlayerBrowse extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId!: string;
@property({ attribute: false }) public entityId?: string;
@property() public action: MediaPlayerBrowseAction = "play";
@ -89,6 +89,8 @@ export class HaMediaPlayerBrowse extends LitElement {
@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
@property({ type: Boolean, reflect: true }) public narrow = false;
@ -250,6 +252,7 @@ export class HaMediaPlayerBrowse extends LitElement {
});
} else if (
err.code === "entity_not_found" &&
this.entityId &&
isUnavailableState(this.hass.states[this.entityId]?.state)
) {
this._setError({
@ -334,7 +337,37 @@ export class HaMediaPlayerBrowse extends LitElement {
const subtitle = this.hass.localize(
`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 childrenMediaClass = 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`
<ha-fab
mini
@ -748,11 +786,11 @@ export class HaMediaPlayerBrowse extends LitElement {
};
private async _fetchData(
entityId: string,
entityId: string | undefined,
mediaContentId?: string,
mediaContentType?: string
): Promise<MediaPlayerItem> {
return entityId !== BROWSER_PLAYER
return entityId && entityId !== BROWSER_PLAYER
? browseMediaPlayer(this.hass, entityId, mediaContentId, mediaContentType)
: browseLocalMediaPlayer(this.hass, mediaContentId);
}

View File

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

View File

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