>
+ ): 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;
+ }
+}
diff --git a/src/components/media-player/ha-media-player-browse.ts b/src/components/media-player/ha-media-player-browse.ts
index 4c4889dbca..eb2a8674d6 100644
--- a/src/components/media-player/ha-media-player-browse.ts
+++ b/src/components/media-player/ha-media-player-browse.ts
@@ -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 {
`
- : isTTSMediaSource(currentItem.media_content_id)
- ? html`
-
- `
- : !children.length && !currentItem.not_shown
+ : isManualMediaSourceContentId(currentItem.media_content_id)
+ ? html``
+ : isTTSMediaSource(currentItem.media_content_id)
? html`
-
- ${currentItem.media_content_id ===
- "media-source://media_source/local/."
- ? html`
-
-
-
-
-
- ${this.hass.localize(
- "ui.components.media-browser.file_management.highlight_button"
- )}
-
-
- `
- : this.hass.localize(
- "ui.components.media-browser.no_items"
- )}
-
+
`
- : this.preferredLayout === "grid" ||
- (this.preferredLayout === "auto" &&
- childrenMediaClass.layout === "grid")
+ : !children.length && !currentItem.not_shown
? html`
-
- ${currentItem.not_shown
- ? html`
-
-
- ${this.hass.localize(
- "ui.components.media-browser.not_shown",
- { count: currentItem.not_shown }
- )}
+
+ ${currentItem.media_content_id ===
+ "media-source://media_source/local/."
+ ? html`
+
+
+
+
+
+ ${this.hass.localize(
+ "ui.components.media-browser.file_management.highlight_button"
+ )}
+
-
- `
- : ""}
+ `
+ : this.hass.localize(
+ "ui.components.media-browser.no_items"
+ )}
+
`
- : html`
-
+ : this.preferredLayout === "grid" ||
+ (this.preferredLayout === "auto" &&
+ childrenMediaClass.layout === "grid")
+ ? html`
${currentItem.not_shown
? html`
-
-
+
+
${this.hass.localize(
"ui.components.media-browser.not_shown",
{ count: currentItem.not_shown }
)}
-
-
+
+
`
: ""}
-
- `
+ `
+ : html`
+
+
+ ${currentItem.not_shown
+ ? html`
+
+
+ ${this.hass.localize(
+ "ui.components.media-browser.not_shown",
+ { count: currentItem.not_shown }
+ )}
+
+
+ `
+ : ""}
+
+ `
}
@@ -617,8 +661,9 @@ export class HaMediaPlayerBrowse extends LitElement {
: html`
) {
+ ev.stopPropagation();
+ fireEvent(this, "media-picked", {
+ item: ev.detail.item as MediaPlayerItem,
+ navigateIds: this.navigateIds,
+ });
+ }
+
private _childClicked = async (ev: MouseEvent): Promise => {
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 {
- 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;
diff --git a/src/components/media-player/show-media-browser-dialog.ts b/src/components/media-player/show-media-browser-dialog.ts
index 5fdfeac789..978e8a2604 100644
--- a/src/components/media-player/show-media-browser-dialog.ts
+++ b/src/components/media-player/show-media-browser-dialog.ts
@@ -12,6 +12,8 @@ export interface MediaPlayerBrowseDialogParams {
navigateIds?: MediaPlayerItemId[];
minimumNavigateLevel?: number;
accept?: string[];
+ defaultId?: string;
+ defaultType?: string;
}
export const showMediaBrowserDialog = (
diff --git a/src/data/media-player.ts b/src/data/media-player.ts
index 6e846162be..968c4b48a5 100644
--- a/src/data/media-player.ts
+++ b/src/data/media-player.ts
@@ -199,10 +199,12 @@ export interface MediaPlayerItem {
media_content_type: string;
media_content_id: string;
media_class: keyof TranslationDict["ui"]["components"]["media-browser"]["class"];
- children_media_class?: string;
+ children_media_class?: string | null;
can_play: boolean;
can_expand: boolean;
+ can_search: boolean;
thumbnail?: string;
+ iconPath?: string;
children?: MediaPlayerItem[];
not_shown?: number;
}
diff --git a/src/data/media_source.ts b/src/data/media_source.ts
index 2015c1f8e7..0717a64765 100644
--- a/src/data/media_source.ts
+++ b/src/data/media_source.ts
@@ -24,6 +24,11 @@ export const browseLocalMediaPlayer = (
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) =>
mediaId.startsWith("media-source://");