mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-09 02:49:51 +00:00
Manual entry mode for media selector (#26753)
This commit is contained in:
@@ -246,6 +246,8 @@ export class HaMediaSelector extends LitElement {
|
||||
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,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
|
||||
@@ -165,6 +165,8 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
.action=${this._action}
|
||||
.preferredLayout=${this._preferredLayout}
|
||||
.accept=${this._params.accept}
|
||||
.defaultId=${this._params.defaultId}
|
||||
.defaultType=${this._params.defaultType}
|
||||
@close-dialog=${this.closeDialog}
|
||||
@media-picked=${this._mediaPicked}
|
||||
@media-browsed=${this._mediaBrowsed}
|
||||
|
||||
112
src/components/media-player/ha-browse-media-manual.ts
Normal file
112
src/components/media-player/ha-browse-media-manual.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button";
|
||||
import "../ha-card";
|
||||
import "../ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../ha-form/types";
|
||||
import type { MediaPlayerItemId } from "./ha-media-player-browse";
|
||||
|
||||
export interface ManualMediaPickedEvent {
|
||||
item: MediaPlayerItemId;
|
||||
}
|
||||
|
||||
@customElement("ha-browse-media-manual")
|
||||
class BrowseMediaManual extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public item!: MediaPlayerItemId;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
name: "media_content_id",
|
||||
required: true,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "media_content_type",
|
||||
required: false,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.schema=${this._schema()}
|
||||
.data=${this.item}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button @click=${this._mediaPicked}>
|
||||
${this.hass.localize("ui.common.submit")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
const value = { ...ev.detail.value };
|
||||
|
||||
this.item = value;
|
||||
}
|
||||
|
||||
private _computeLabel = (
|
||||
entry: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(`ui.components.selectors.media.${entry.name}`);
|
||||
|
||||
private _computeHelper = (
|
||||
entry: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(`ui.components.selectors.media.${entry.name}_detail`);
|
||||
|
||||
private _mediaPicked() {
|
||||
fireEvent(this, "manual-media-picked", {
|
||||
item: {
|
||||
media_content_id: this.item.media_content_id || "",
|
||||
media_content_type: this.item.media_content_type || "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
margin: 16px auto;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 448px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-browse-media-manual": BrowseMediaManual;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"manual-media-picked": ManualMediaPickedEvent;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||
import { grid } from "@lit-labs/virtualizer/layouts/grid";
|
||||
|
||||
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
|
||||
import { mdiArrowUpRight, mdiPlay, mdiPlus, mdiKeyboard } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
@@ -28,7 +28,11 @@ import {
|
||||
BROWSER_PLAYER,
|
||||
MediaClassBrowserSettings,
|
||||
} from "../../data/media-player";
|
||||
import { browseLocalMediaPlayer } from "../../data/media_source";
|
||||
import {
|
||||
browseLocalMediaPlayer,
|
||||
isManualMediaSourceContentId,
|
||||
MANUAL_MEDIA_SOURCE_PREFIX,
|
||||
} from "../../data/media_source";
|
||||
import { isTTSMediaSource } from "../../data/tts";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
@@ -53,7 +57,9 @@ import "../ha-spinner";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
import "./ha-browse-media-tts";
|
||||
import "./ha-browse-media-manual";
|
||||
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
|
||||
import type { ManualMediaPickedEvent } from "./ha-browse-media-manual";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -74,6 +80,18 @@ export interface MediaPlayerItemId {
|
||||
media_content_type: string | undefined;
|
||||
}
|
||||
|
||||
const MANUAL_ITEM: MediaPlayerItem = {
|
||||
can_expand: true,
|
||||
can_play: false,
|
||||
can_search: false,
|
||||
children_media_class: "",
|
||||
media_class: "app",
|
||||
media_content_id: MANUAL_MEDIA_SOURCE_PREFIX,
|
||||
media_content_type: "",
|
||||
iconPath: mdiKeyboard,
|
||||
title: "Manual entry",
|
||||
};
|
||||
|
||||
@customElement("ha-media-player-browse")
|
||||
export class HaMediaPlayerBrowse extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -91,6 +109,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public accept?: string[];
|
||||
|
||||
@property({ attribute: false }) public defaultId?: string;
|
||||
|
||||
@property({ attribute: false }) public defaultType?: string;
|
||||
|
||||
// @todo Consider reworking to eliminate need for attribute since it is manipulated internally
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@@ -216,56 +238,69 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
}
|
||||
// Fetch current
|
||||
if (!currentProm) {
|
||||
currentProm = this._fetchData(
|
||||
this.entityId,
|
||||
currentId.media_content_id,
|
||||
currentId.media_content_type
|
||||
if (
|
||||
currentId.media_content_id &&
|
||||
isManualMediaSourceContentId(currentId.media_content_id)
|
||||
) {
|
||||
this._currentItem = MANUAL_ITEM;
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: navigateIds,
|
||||
current: this._currentItem,
|
||||
});
|
||||
} else {
|
||||
if (!currentProm) {
|
||||
currentProm = this._fetchData(
|
||||
this.entityId,
|
||||
currentId.media_content_id,
|
||||
currentId.media_content_type
|
||||
);
|
||||
}
|
||||
currentProm.then(
|
||||
(item) => {
|
||||
this._currentItem = item;
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: navigateIds,
|
||||
current: item,
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
// When we change entity ID, we will first try to see if the new entity is
|
||||
// able to resolve the new path. If that results in an error, browse the root.
|
||||
const isNewEntityWithSamePath =
|
||||
oldNavigateIds &&
|
||||
changedProps.has("entityId") &&
|
||||
navigateIds.length === oldNavigateIds.length &&
|
||||
oldNavigateIds.every(
|
||||
(oldItem, idx) =>
|
||||
navigateIds[idx].media_content_id ===
|
||||
oldItem.media_content_id &&
|
||||
navigateIds[idx].media_content_type ===
|
||||
oldItem.media_content_type
|
||||
);
|
||||
if (isNewEntityWithSamePath) {
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: [
|
||||
{ media_content_id: undefined, media_content_type: undefined },
|
||||
],
|
||||
replace: true,
|
||||
});
|
||||
} else if (
|
||||
err.code === "entity_not_found" &&
|
||||
this.entityId &&
|
||||
isUnavailableState(this.hass.states[this.entityId]?.state)
|
||||
) {
|
||||
this._setError({
|
||||
message: this.hass.localize(
|
||||
`ui.components.media-browser.media_player_unavailable`
|
||||
),
|
||||
code: "entity_not_found",
|
||||
});
|
||||
} else {
|
||||
this._setError(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
currentProm.then(
|
||||
(item) => {
|
||||
this._currentItem = item;
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: navigateIds,
|
||||
current: item,
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
// When we change entity ID, we will first try to see if the new entity is
|
||||
// able to resolve the new path. If that results in an error, browse the root.
|
||||
const isNewEntityWithSamePath =
|
||||
oldNavigateIds &&
|
||||
changedProps.has("entityId") &&
|
||||
navigateIds.length === oldNavigateIds.length &&
|
||||
oldNavigateIds.every(
|
||||
(oldItem, idx) =>
|
||||
navigateIds[idx].media_content_id === oldItem.media_content_id &&
|
||||
navigateIds[idx].media_content_type === oldItem.media_content_type
|
||||
);
|
||||
if (isNewEntityWithSamePath) {
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: [
|
||||
{ media_content_id: undefined, media_content_type: undefined },
|
||||
],
|
||||
replace: true,
|
||||
});
|
||||
} else if (
|
||||
err.code === "entity_not_found" &&
|
||||
this.entityId &&
|
||||
isUnavailableState(this.hass.states[this.entityId]?.state)
|
||||
) {
|
||||
this._setError({
|
||||
message: this.hass.localize(
|
||||
`ui.components.media-browser.media_player_unavailable`
|
||||
),
|
||||
code: "entity_not_found",
|
||||
});
|
||||
} else {
|
||||
this._setError(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
// Fetch parent
|
||||
if (!parentProm && parentId !== undefined) {
|
||||
parentProm = this._fetchData(
|
||||
@@ -479,111 +514,120 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
</ha-alert>
|
||||
</div>
|
||||
`
|
||||
: isTTSMediaSource(currentItem.media_content_id)
|
||||
? html`
|
||||
<ha-browse-media-tts
|
||||
.item=${currentItem}
|
||||
.hass=${this.hass}
|
||||
.action=${this.action}
|
||||
@tts-picked=${this._ttsPicked}
|
||||
></ha-browse-media-tts>
|
||||
`
|
||||
: !children.length && !currentItem.not_shown
|
||||
: isManualMediaSourceContentId(currentItem.media_content_id)
|
||||
? html`<ha-browse-media-manual
|
||||
.item=${{
|
||||
media_content_id: this.defaultId || "",
|
||||
media_content_type: this.defaultType || "",
|
||||
}}
|
||||
.hass=${this.hass}
|
||||
@manual-media-picked=${this._manualPicked}
|
||||
></ha-browse-media-manual>`
|
||||
: isTTSMediaSource(currentItem.media_content_id)
|
||||
? html`
|
||||
<div class="container no-items">
|
||||
${currentItem.media_content_id ===
|
||||
"media-source://media_source/local/."
|
||||
? html`
|
||||
<div class="highlight-add-button">
|
||||
<span>
|
||||
<ha-svg-icon
|
||||
.path=${mdiArrowUpRight}
|
||||
></ha-svg-icon>
|
||||
</span>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.highlight_button"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.components.media-browser.no_items"
|
||||
)}
|
||||
</div>
|
||||
<ha-browse-media-tts
|
||||
.item=${currentItem}
|
||||
.hass=${this.hass}
|
||||
.action=${this.action}
|
||||
@tts-picked=${this._ttsPicked}
|
||||
></ha-browse-media-tts>
|
||||
`
|
||||
: this.preferredLayout === "grid" ||
|
||||
(this.preferredLayout === "auto" &&
|
||||
childrenMediaClass.layout === "grid")
|
||||
: !children.length && !currentItem.not_shown
|
||||
? html`
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
.layout=${grid({
|
||||
itemSize: {
|
||||
width: "175px",
|
||||
height:
|
||||
childrenMediaClass.thumbnail_ratio ===
|
||||
"portrait"
|
||||
? "312px"
|
||||
: "225px",
|
||||
},
|
||||
gap: "16px",
|
||||
flex: { preserve: "aspect-ratio" },
|
||||
justify: "space-evenly",
|
||||
direction: "vertical",
|
||||
})}
|
||||
.items=${children}
|
||||
.renderItem=${this._renderGridItem}
|
||||
class="children ${classMap({
|
||||
portrait:
|
||||
childrenMediaClass.thumbnail_ratio ===
|
||||
"portrait",
|
||||
not_shown: !!currentItem.not_shown,
|
||||
})}"
|
||||
></lit-virtualizer>
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
<div class="grid not-shown">
|
||||
<div class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
)}
|
||||
<div class="container no-items">
|
||||
${currentItem.media_content_id ===
|
||||
"media-source://media_source/local/."
|
||||
? html`
|
||||
<div class="highlight-add-button">
|
||||
<span>
|
||||
<ha-svg-icon
|
||||
.path=${mdiArrowUpRight}
|
||||
></ha-svg-icon>
|
||||
</span>
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.highlight_button"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.components.media-browser.no_items"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-list>
|
||||
: this.preferredLayout === "grid" ||
|
||||
(this.preferredLayout === "auto" &&
|
||||
childrenMediaClass.layout === "grid")
|
||||
? html`
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
.items=${children}
|
||||
style=${styleMap({
|
||||
height: `${children.length * 72 + 26}px`,
|
||||
.layout=${grid({
|
||||
itemSize: {
|
||||
width: "175px",
|
||||
height:
|
||||
childrenMediaClass.thumbnail_ratio ===
|
||||
"portrait"
|
||||
? "312px"
|
||||
: "225px",
|
||||
},
|
||||
gap: "16px",
|
||||
flex: { preserve: "aspect-ratio" },
|
||||
justify: "space-evenly",
|
||||
direction: "vertical",
|
||||
})}
|
||||
.renderItem=${this._renderListItem}
|
||||
.items=${children}
|
||||
.renderItem=${this._renderGridItem}
|
||||
class="children ${classMap({
|
||||
portrait:
|
||||
childrenMediaClass.thumbnail_ratio ===
|
||||
"portrait",
|
||||
not_shown: !!currentItem.not_shown,
|
||||
})}"
|
||||
></lit-virtualizer>
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
<ha-list-item
|
||||
noninteractive
|
||||
class="not-shown"
|
||||
.graphic=${mediaClass.show_list_images
|
||||
? "medium"
|
||||
: "avatar"}
|
||||
>
|
||||
<span class="title">
|
||||
<div class="grid not-shown">
|
||||
<div class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
)}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-list>
|
||||
`
|
||||
`
|
||||
: html`
|
||||
<ha-list>
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
.items=${children}
|
||||
style=${styleMap({
|
||||
height: `${children.length * 72 + 26}px`,
|
||||
})}
|
||||
.renderItem=${this._renderListItem}
|
||||
></lit-virtualizer>
|
||||
${currentItem.not_shown
|
||||
? html`
|
||||
<ha-list-item
|
||||
noninteractive
|
||||
class="not-shown"
|
||||
.graphic=${mediaClass.show_list_images
|
||||
? "medium"
|
||||
: "avatar"}
|
||||
>
|
||||
<span class="title">
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.not_shown",
|
||||
{ count: currentItem.not_shown }
|
||||
)}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
: ""}
|
||||
</ha-list>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -617,8 +661,9 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${MediaClassBrowserSettings[
|
||||
class=${child.iconPath ? "icon" : "folder"}
|
||||
.path=${child.iconPath ||
|
||||
MediaClassBrowserSettings[
|
||||
child.media_class === "directory"
|
||||
? child.children_media_class || child.media_class
|
||||
: child.media_class
|
||||
@@ -768,6 +813,14 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _manualPicked(ev: CustomEvent<ManualMediaPickedEvent>) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "media-picked", {
|
||||
item: ev.detail.item as MediaPlayerItem,
|
||||
navigateIds: this.navigateIds,
|
||||
});
|
||||
}
|
||||
|
||||
private _childClicked = async (ev: MouseEvent): Promise<void> => {
|
||||
const target = ev.currentTarget as any;
|
||||
const item: MediaPlayerItem = target.item;
|
||||
@@ -791,9 +844,23 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
mediaContentId?: string,
|
||||
mediaContentType?: string
|
||||
): Promise<MediaPlayerItem> {
|
||||
return entityId && entityId !== BROWSER_PLAYER
|
||||
? browseMediaPlayer(this.hass, entityId, mediaContentId, mediaContentType)
|
||||
: browseLocalMediaPlayer(this.hass, mediaContentId);
|
||||
const prom =
|
||||
entityId && entityId !== BROWSER_PLAYER
|
||||
? browseMediaPlayer(
|
||||
this.hass,
|
||||
entityId,
|
||||
mediaContentId,
|
||||
mediaContentType
|
||||
)
|
||||
: browseLocalMediaPlayer(this.hass, mediaContentId);
|
||||
|
||||
return prom.then((item) => {
|
||||
if (!mediaContentId && this.action === "pick") {
|
||||
item.children = item.children || [];
|
||||
item.children.push(MANUAL_ITEM);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
private _measureCard(): void {
|
||||
@@ -1141,6 +1208,11 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
||||
}
|
||||
|
||||
.child .icon {
|
||||
color: #00a9f7; /* Match the png color from brands repo */
|
||||
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
||||
}
|
||||
|
||||
.child .play {
|
||||
position: absolute;
|
||||
transition: color 0.5s;
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface MediaPlayerBrowseDialogParams {
|
||||
navigateIds?: MediaPlayerItemId[];
|
||||
minimumNavigateLevel?: number;
|
||||
accept?: string[];
|
||||
defaultId?: string;
|
||||
defaultType?: string;
|
||||
}
|
||||
|
||||
export const showMediaBrowserDialog = (
|
||||
|
||||
Reference in New Issue
Block a user