mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-09 10:59:50 +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(),
|
entityId: this._getActiveEntityId(),
|
||||||
navigateIds: this.value?.metadata?.navigateIds,
|
navigateIds: this.value?.metadata?.navigateIds,
|
||||||
accept: this.selector.media?.accept,
|
accept: this.selector.media?.accept,
|
||||||
|
defaultId: this.value?.media_content_id,
|
||||||
|
defaultType: this.value?.media_content_type,
|
||||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
|
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: {
|
value: {
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ class DialogMediaPlayerBrowse extends LitElement {
|
|||||||
.action=${this._action}
|
.action=${this._action}
|
||||||
.preferredLayout=${this._preferredLayout}
|
.preferredLayout=${this._preferredLayout}
|
||||||
.accept=${this._params.accept}
|
.accept=${this._params.accept}
|
||||||
|
.defaultId=${this._params.defaultId}
|
||||||
|
.defaultType=${this._params.defaultType}
|
||||||
@close-dialog=${this.closeDialog}
|
@close-dialog=${this.closeDialog}
|
||||||
@media-picked=${this._mediaPicked}
|
@media-picked=${this._mediaPicked}
|
||||||
@media-browsed=${this._mediaBrowsed}
|
@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 type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||||
import { grid } from "@lit-labs/virtualizer/layouts/grid";
|
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 type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import {
|
import {
|
||||||
@@ -28,7 +28,11 @@ import {
|
|||||||
BROWSER_PLAYER,
|
BROWSER_PLAYER,
|
||||||
MediaClassBrowserSettings,
|
MediaClassBrowserSettings,
|
||||||
} from "../../data/media-player";
|
} 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 { isTTSMediaSource } from "../../data/tts";
|
||||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||||
import { haStyle } from "../../resources/styles";
|
import { haStyle } from "../../resources/styles";
|
||||||
@@ -53,7 +57,9 @@ import "../ha-spinner";
|
|||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "../ha-tooltip";
|
import "../ha-tooltip";
|
||||||
import "./ha-browse-media-tts";
|
import "./ha-browse-media-tts";
|
||||||
|
import "./ha-browse-media-manual";
|
||||||
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
|
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
|
||||||
|
import type { ManualMediaPickedEvent } from "./ha-browse-media-manual";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
@@ -74,6 +80,18 @@ export interface MediaPlayerItemId {
|
|||||||
media_content_type: string | undefined;
|
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")
|
@customElement("ha-media-player-browse")
|
||||||
export class HaMediaPlayerBrowse extends LitElement {
|
export class HaMediaPlayerBrowse extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@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 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
|
// @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;
|
||||||
|
|
||||||
@@ -216,6 +238,16 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fetch current
|
// Fetch current
|
||||||
|
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) {
|
if (!currentProm) {
|
||||||
currentProm = this._fetchData(
|
currentProm = this._fetchData(
|
||||||
this.entityId,
|
this.entityId,
|
||||||
@@ -240,8 +272,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
navigateIds.length === oldNavigateIds.length &&
|
navigateIds.length === oldNavigateIds.length &&
|
||||||
oldNavigateIds.every(
|
oldNavigateIds.every(
|
||||||
(oldItem, idx) =>
|
(oldItem, idx) =>
|
||||||
navigateIds[idx].media_content_id === oldItem.media_content_id &&
|
navigateIds[idx].media_content_id ===
|
||||||
navigateIds[idx].media_content_type === oldItem.media_content_type
|
oldItem.media_content_id &&
|
||||||
|
navigateIds[idx].media_content_type ===
|
||||||
|
oldItem.media_content_type
|
||||||
);
|
);
|
||||||
if (isNewEntityWithSamePath) {
|
if (isNewEntityWithSamePath) {
|
||||||
fireEvent(this, "media-browsed", {
|
fireEvent(this, "media-browsed", {
|
||||||
@@ -266,6 +300,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
}
|
||||||
// Fetch parent
|
// Fetch parent
|
||||||
if (!parentProm && parentId !== undefined) {
|
if (!parentProm && parentId !== undefined) {
|
||||||
parentProm = this._fetchData(
|
parentProm = this._fetchData(
|
||||||
@@ -479,6 +514,15 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
</ha-alert>
|
</ha-alert>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
: 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)
|
: isTTSMediaSource(currentItem.media_content_id)
|
||||||
? html`
|
? html`
|
||||||
<ha-browse-media-tts
|
<ha-browse-media-tts
|
||||||
@@ -617,8 +661,9 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
: html`
|
: html`
|
||||||
<div class="icon-holder image">
|
<div class="icon-holder image">
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
class="folder"
|
class=${child.iconPath ? "icon" : "folder"}
|
||||||
.path=${MediaClassBrowserSettings[
|
.path=${child.iconPath ||
|
||||||
|
MediaClassBrowserSettings[
|
||||||
child.media_class === "directory"
|
child.media_class === "directory"
|
||||||
? child.children_media_class || child.media_class
|
? child.children_media_class || child.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> => {
|
private _childClicked = async (ev: MouseEvent): Promise<void> => {
|
||||||
const target = ev.currentTarget as any;
|
const target = ev.currentTarget as any;
|
||||||
const item: MediaPlayerItem = target.item;
|
const item: MediaPlayerItem = target.item;
|
||||||
@@ -791,9 +844,23 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
mediaContentId?: string,
|
mediaContentId?: string,
|
||||||
mediaContentType?: string
|
mediaContentType?: string
|
||||||
): Promise<MediaPlayerItem> {
|
): Promise<MediaPlayerItem> {
|
||||||
return entityId && entityId !== BROWSER_PLAYER
|
const prom =
|
||||||
? browseMediaPlayer(this.hass, entityId, mediaContentId, mediaContentType)
|
entityId && entityId !== BROWSER_PLAYER
|
||||||
|
? browseMediaPlayer(
|
||||||
|
this.hass,
|
||||||
|
entityId,
|
||||||
|
mediaContentId,
|
||||||
|
mediaContentType
|
||||||
|
)
|
||||||
: browseLocalMediaPlayer(this.hass, mediaContentId);
|
: 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 {
|
private _measureCard(): void {
|
||||||
@@ -1141,6 +1208,11 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
--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 {
|
.child .play {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transition: color 0.5s;
|
transition: color 0.5s;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export interface MediaPlayerBrowseDialogParams {
|
|||||||
navigateIds?: MediaPlayerItemId[];
|
navigateIds?: MediaPlayerItemId[];
|
||||||
minimumNavigateLevel?: number;
|
minimumNavigateLevel?: number;
|
||||||
accept?: string[];
|
accept?: string[];
|
||||||
|
defaultId?: string;
|
||||||
|
defaultType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showMediaBrowserDialog = (
|
export const showMediaBrowserDialog = (
|
||||||
|
|||||||
@@ -199,10 +199,12 @@ export interface MediaPlayerItem {
|
|||||||
media_content_type: string;
|
media_content_type: string;
|
||||||
media_content_id: string;
|
media_content_id: string;
|
||||||
media_class: keyof TranslationDict["ui"]["components"]["media-browser"]["class"];
|
media_class: keyof TranslationDict["ui"]["components"]["media-browser"]["class"];
|
||||||
children_media_class?: string;
|
children_media_class?: string | null;
|
||||||
can_play: boolean;
|
can_play: boolean;
|
||||||
can_expand: boolean;
|
can_expand: boolean;
|
||||||
|
can_search: boolean;
|
||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
|
iconPath?: string;
|
||||||
children?: MediaPlayerItem[];
|
children?: MediaPlayerItem[];
|
||||||
not_shown?: number;
|
not_shown?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ export const browseLocalMediaPlayer = (
|
|||||||
media_content_id: mediaContentId,
|
media_content_id: mediaContentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const MANUAL_MEDIA_SOURCE_PREFIX = "__MANUAL_ENTRY__";
|
||||||
|
|
||||||
|
export const isManualMediaSourceContentId = (mediaContentId: string) =>
|
||||||
|
mediaContentId.startsWith(MANUAL_MEDIA_SOURCE_PREFIX);
|
||||||
|
|
||||||
export const isMediaSourceContentId = (mediaId: string) =>
|
export const isMediaSourceContentId = (mediaId: string) =>
|
||||||
mediaId.startsWith("media-source://");
|
mediaId.startsWith("media-source://");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user