mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-21 08:16:36 +00:00
Media Browser Panel (#6772)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
b065f002a4
commit
e63a78bcdb
@ -3,56 +3,39 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import { getExternalConfig } from "../external_app/external_config";
|
||||
import {
|
||||
CAMERA_SUPPORT_STREAM,
|
||||
computeMJPEGStreamUrl,
|
||||
fetchStreamUrl,
|
||||
} from "../data/camera";
|
||||
import { CameraEntity, HomeAssistant } from "../types";
|
||||
|
||||
type HLSModule = typeof import("hls.js");
|
||||
import "./ha-hls-player";
|
||||
|
||||
@customElement("ha-camera-stream")
|
||||
class HaCameraStream extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public stateObj?: CameraEntity;
|
||||
@property({ attribute: false }) public stateObj?: CameraEntity;
|
||||
|
||||
@property({ type: Boolean }) public showControls = false;
|
||||
|
||||
@internalProperty() private _attached = false;
|
||||
|
||||
// We keep track if we should force MJPEG with a string
|
||||
// that way it automatically resets if we change entity.
|
||||
@internalProperty() private _forceMJPEG: string | undefined = undefined;
|
||||
@internalProperty() private _forceMJPEG?: string;
|
||||
|
||||
private _hlsPolyfillInstance?: Hls;
|
||||
|
||||
private _useExoPlayer = false;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._attached = true;
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._attached = false;
|
||||
}
|
||||
@internalProperty() private _url?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.stateObj || !this._attached) {
|
||||
if (!this.stateObj || (!this._forceMJPEG && !this._url)) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@ -70,50 +53,22 @@ class HaCameraStream extends LitElement {
|
||||
/>
|
||||
`
|
||||
: html`
|
||||
<video
|
||||
<ha-hls-player
|
||||
autoplay
|
||||
muted
|
||||
playsinline
|
||||
?controls=${this.showControls}
|
||||
@loadeddata=${this._elementResized}
|
||||
></video>
|
||||
.hass=${this.hass}
|
||||
.url=${this._url!}
|
||||
></ha-hls-player>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
const stateObjChanged = changedProps.has("stateObj");
|
||||
const attachedChanged = changedProps.has("_attached");
|
||||
|
||||
const oldState = changedProps.get("stateObj") as this["stateObj"];
|
||||
const oldEntityId = oldState ? oldState.entity_id : undefined;
|
||||
const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined;
|
||||
|
||||
if (
|
||||
(!stateObjChanged && !attachedChanged) ||
|
||||
(stateObjChanged && oldEntityId === curEntityId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are no longer attached, destroy polyfill.
|
||||
if (attachedChanged && !this._attached) {
|
||||
this._destroyPolyfill();
|
||||
return;
|
||||
}
|
||||
|
||||
// Nothing to do if we are render MJPEG.
|
||||
if (this._shouldRenderMJPEG) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Tear down existing polyfill, if available
|
||||
this._destroyPolyfill();
|
||||
|
||||
if (curEntityId) {
|
||||
this._startHls();
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("stateObj")) {
|
||||
this._forceMJPEG = undefined;
|
||||
this._getStreamUrl();
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,136 +80,35 @@ class HaCameraStream extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private get _videoEl(): HTMLVideoElement {
|
||||
return this.shadowRoot!.querySelector("video")!;
|
||||
}
|
||||
|
||||
private async _getUseExoPlayer(): Promise<boolean> {
|
||||
if (!this.hass!.auth.external) {
|
||||
return false;
|
||||
}
|
||||
const externalConfig = await getExternalConfig(this.hass!.auth.external);
|
||||
return externalConfig && externalConfig.hasExoPlayer;
|
||||
}
|
||||
|
||||
private async _startHls(): Promise<void> {
|
||||
// eslint-disable-next-line
|
||||
let hls;
|
||||
const videoEl = this._videoEl;
|
||||
this._useExoPlayer = await this._getUseExoPlayer();
|
||||
if (!this._useExoPlayer) {
|
||||
hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
|
||||
.default as HLSModule;
|
||||
let hlsSupported = hls.isSupported();
|
||||
|
||||
if (!hlsSupported) {
|
||||
hlsSupported =
|
||||
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
|
||||
}
|
||||
|
||||
if (!hlsSupported) {
|
||||
this._forceMJPEG = this.stateObj!.entity_id;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async _getStreamUrl(): Promise<void> {
|
||||
try {
|
||||
const { url } = await fetchStreamUrl(
|
||||
this.hass!,
|
||||
this.stateObj!.entity_id
|
||||
);
|
||||
|
||||
if (this._useExoPlayer) {
|
||||
this._renderHLSExoPlayer(url);
|
||||
} else if (hls.isSupported()) {
|
||||
this._renderHLSPolyfill(videoEl, hls, url);
|
||||
} else {
|
||||
this._renderHLSNative(videoEl, url);
|
||||
}
|
||||
return;
|
||||
this._url = url;
|
||||
} catch (err) {
|
||||
// Fails if we were unable to get a stream
|
||||
// eslint-disable-next-line
|
||||
console.error(err);
|
||||
|
||||
this._forceMJPEG = this.stateObj!.entity_id;
|
||||
}
|
||||
}
|
||||
|
||||
private async _renderHLSExoPlayer(url: string) {
|
||||
window.addEventListener("resize", this._resizeExoPlayer);
|
||||
this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer);
|
||||
this._videoEl.style.visibility = "hidden";
|
||||
await this.hass!.auth.external!.sendMessage({
|
||||
type: "exoplayer/play_hls",
|
||||
payload: new URL(url, window.location.href).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
private _resizeExoPlayer = () => {
|
||||
const rect = this._videoEl.getBoundingClientRect();
|
||||
this.hass!.auth.external!.fireMessage({
|
||||
type: "exoplayer/resize",
|
||||
payload: {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
|
||||
videoEl.src = url;
|
||||
await new Promise((resolve) =>
|
||||
videoEl.addEventListener("loadedmetadata", resolve)
|
||||
);
|
||||
videoEl.play();
|
||||
}
|
||||
|
||||
private async _renderHLSPolyfill(
|
||||
videoEl: HTMLVideoElement,
|
||||
// eslint-disable-next-line
|
||||
Hls: HLSModule,
|
||||
url: string
|
||||
) {
|
||||
const hls = new Hls({
|
||||
liveBackBufferLength: 60,
|
||||
fragLoadingTimeOut: 30000,
|
||||
manifestLoadingTimeOut: 30000,
|
||||
levelLoadingTimeOut: 30000,
|
||||
});
|
||||
this._hlsPolyfillInstance = hls;
|
||||
hls.attachMedia(videoEl);
|
||||
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||
hls.loadSource(url);
|
||||
});
|
||||
}
|
||||
|
||||
private _elementResized() {
|
||||
fireEvent(this, "iron-resize");
|
||||
}
|
||||
|
||||
private _destroyPolyfill() {
|
||||
if (this._hlsPolyfillInstance) {
|
||||
this._hlsPolyfillInstance.destroy();
|
||||
this._hlsPolyfillInstance = undefined;
|
||||
}
|
||||
if (this._useExoPlayer) {
|
||||
window.removeEventListener("resize", this._resizeExoPlayer);
|
||||
this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host,
|
||||
img,
|
||||
video {
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
@ -10,7 +10,7 @@ import "./ha-icon-button";
|
||||
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
|
||||
|
||||
export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
|
||||
${title}
|
||||
<span class="header_title">${title}</span>
|
||||
<mwc-icon-button
|
||||
aria-label=${hass.localize("ui.dialogs.generic.close")}
|
||||
dialogAction="close"
|
||||
@ -77,10 +77,17 @@ export class HaDialog extends MwcDialog {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
.header_title {
|
||||
margin-right: 40px;
|
||||
}
|
||||
[dir="rtl"].header_button {
|
||||
right: auto;
|
||||
left: 16px;
|
||||
}
|
||||
[dir="rtl"].header_title {
|
||||
margin-left: 40px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
216
src/components/ha-hls-player.ts
Normal file
216
src/components/ha-hls-player.ts
Normal file
@ -0,0 +1,216 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import { getExternalConfig } from "../external_app/external_config";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
type HLSModule = typeof import("hls.js");
|
||||
|
||||
@customElement("ha-hls-player")
|
||||
class HaHLSPlayer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public url!: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "controls" })
|
||||
public controls = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "muted" })
|
||||
public muted = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "autoplay" })
|
||||
public autoPlay = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "playsinline" })
|
||||
public playsInline = false;
|
||||
|
||||
@query("video") private _videoEl!: HTMLVideoElement;
|
||||
|
||||
@internalProperty() private _attached = false;
|
||||
|
||||
private _hlsPolyfillInstance?: Hls;
|
||||
|
||||
private _useExoPlayer = false;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._attached = true;
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._attached = false;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._attached) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<video
|
||||
?autoplay=${this.autoPlay}
|
||||
?muted=${this.muted}
|
||||
?playsinline=${this.playsInline}
|
||||
?controls=${this.controls}
|
||||
@loadeddata=${this._elementResized}
|
||||
></video>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
const attachedChanged = changedProps.has("_attached");
|
||||
const urlChanged = changedProps.has("url");
|
||||
|
||||
if (!urlChanged && !attachedChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are no longer attached, destroy polyfill
|
||||
if (attachedChanged && !this._attached) {
|
||||
// Tear down existing polyfill, if available
|
||||
this._destroyPolyfill();
|
||||
return;
|
||||
}
|
||||
|
||||
this._destroyPolyfill();
|
||||
this._startHls();
|
||||
}
|
||||
|
||||
private async _getUseExoPlayer(): Promise<boolean> {
|
||||
if (!this.hass!.auth.external) {
|
||||
return false;
|
||||
}
|
||||
const externalConfig = await getExternalConfig(this.hass!.auth.external);
|
||||
return externalConfig && externalConfig.hasExoPlayer;
|
||||
}
|
||||
|
||||
private async _startHls(): Promise<void> {
|
||||
let hls: any;
|
||||
const videoEl = this._videoEl;
|
||||
this._useExoPlayer = await this._getUseExoPlayer();
|
||||
if (!this._useExoPlayer) {
|
||||
hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
|
||||
.default as HLSModule;
|
||||
let hlsSupported = hls.isSupported();
|
||||
|
||||
if (!hlsSupported) {
|
||||
hlsSupported =
|
||||
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
|
||||
}
|
||||
|
||||
if (!hlsSupported) {
|
||||
this._videoEl.innerHTML = this.hass.localize(
|
||||
"ui.components.media-browser.video_not_supported"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const url = this.url;
|
||||
|
||||
if (this._useExoPlayer) {
|
||||
this._renderHLSExoPlayer(url);
|
||||
} else if (hls.isSupported()) {
|
||||
this._renderHLSPolyfill(videoEl, hls, url);
|
||||
} else {
|
||||
this._renderHLSNative(videoEl, url);
|
||||
}
|
||||
}
|
||||
|
||||
private async _renderHLSExoPlayer(url: string) {
|
||||
window.addEventListener("resize", this._resizeExoPlayer);
|
||||
this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer);
|
||||
this._videoEl.style.visibility = "hidden";
|
||||
await this.hass!.auth.external!.sendMessage({
|
||||
type: "exoplayer/play_hls",
|
||||
payload: new URL(url, window.location.href).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
private _resizeExoPlayer = () => {
|
||||
const rect = this._videoEl.getBoundingClientRect();
|
||||
this.hass!.auth.external!.fireMessage({
|
||||
type: "exoplayer/resize",
|
||||
payload: {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private async _renderHLSPolyfill(
|
||||
videoEl: HTMLVideoElement,
|
||||
Hls: HLSModule,
|
||||
url: string
|
||||
) {
|
||||
const hls = new Hls({
|
||||
liveBackBufferLength: 60,
|
||||
fragLoadingTimeOut: 30000,
|
||||
manifestLoadingTimeOut: 30000,
|
||||
levelLoadingTimeOut: 30000,
|
||||
});
|
||||
this._hlsPolyfillInstance = hls;
|
||||
hls.attachMedia(videoEl);
|
||||
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||
hls.loadSource(url);
|
||||
});
|
||||
}
|
||||
|
||||
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
|
||||
videoEl.src = url;
|
||||
await new Promise((resolve) =>
|
||||
videoEl.addEventListener("loadedmetadata", resolve)
|
||||
);
|
||||
videoEl.play();
|
||||
}
|
||||
|
||||
private _elementResized() {
|
||||
fireEvent(this, "iron-resize");
|
||||
}
|
||||
|
||||
private _destroyPolyfill() {
|
||||
if (this._hlsPolyfillInstance) {
|
||||
this._hlsPolyfillInstance.destroy();
|
||||
this._hlsPolyfillInstance = undefined;
|
||||
}
|
||||
if (this._useExoPlayer) {
|
||||
window.removeEventListener("resize", this._resizeExoPlayer);
|
||||
this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host,
|
||||
video {
|
||||
display: block;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-hls-player": HaHLSPlayer;
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
@ -33,16 +33,17 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
|
||||
@internalProperty() private _params?: MediaPlayerBrowseDialogParams;
|
||||
|
||||
public async showDialog(
|
||||
params: MediaPlayerBrowseDialogParams
|
||||
): Promise<void> {
|
||||
public showDialog(params: MediaPlayerBrowseDialogParams): void {
|
||||
this._params = params;
|
||||
this._entityId = this._params.entityId;
|
||||
this._mediaContentId = this._params.mediaContentId;
|
||||
this._mediaContentType = this._params.mediaContentType;
|
||||
this._action = this._params.action || "play";
|
||||
}
|
||||
|
||||
await this.updateComplete;
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", {dialog: this.localName});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@ -57,7 +58,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
flexContent
|
||||
@closed=${this._closeDialog}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<ha-media-player-browse
|
||||
dialog
|
||||
@ -66,21 +67,17 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
.action=${this._action!}
|
||||
.mediaContentId=${this._mediaContentId}
|
||||
.mediaContentType=${this._mediaContentType}
|
||||
@close-dialog=${this._closeDialog}
|
||||
@close-dialog=${this.closeDialog}
|
||||
@media-picked=${this._mediaPicked}
|
||||
></ha-media-player-browse>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _closeDialog() {
|
||||
this._params = undefined;
|
||||
}
|
||||
|
||||
private _mediaPicked(ev: HASSDomEvent<MediaPickedEvent>): void {
|
||||
this._params!.mediaPickedCallback(ev.detail);
|
||||
if (this._action !== "play") {
|
||||
this._closeDialog();
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,14 +90,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
|
@ -22,7 +22,13 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { browseMediaPlayer, MediaPickedEvent } from "../../data/media-player";
|
||||
import {
|
||||
browseLocalMediaPlayer,
|
||||
browseMediaPlayer,
|
||||
BROWSER_SOURCE,
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
} from "../../data/media-player";
|
||||
import type { MediaPlayerItem } from "../../data/media-player";
|
||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
@ -50,11 +56,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
|
||||
@property() public mediaContentType?: string;
|
||||
|
||||
@property() public action: "pick" | "play" = "play";
|
||||
|
||||
@property({ type: Boolean }) public hideBack = false;
|
||||
|
||||
@property({ type: Boolean }) public hideTitle = false;
|
||||
@property() public action: MediaPlayerBrowseAction = "play";
|
||||
|
||||
@property({ type: Boolean }) public dialog = false;
|
||||
|
||||
@ -88,52 +90,53 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._mediaPlayerItems.length) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (this._loading) {
|
||||
return html`<ha-circular-progress active></ha-circular-progress>`;
|
||||
}
|
||||
|
||||
const mostRecentItem = this._mediaPlayerItems[
|
||||
if (!this._mediaPlayerItems.length) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const currentItem = this._mediaPlayerItems[
|
||||
this._mediaPlayerItems.length - 1
|
||||
];
|
||||
const previousItem =
|
||||
|
||||
const previousItem: MediaPlayerItem | undefined =
|
||||
this._mediaPlayerItems.length > 1
|
||||
? this._mediaPlayerItems[this._mediaPlayerItems.length - 2]
|
||||
: undefined;
|
||||
|
||||
const hasExpandableChildren:
|
||||
| MediaPlayerItem
|
||||
| undefined = this._hasExpandableChildren(mostRecentItem.children);
|
||||
| undefined = this._hasExpandableChildren(currentItem.children);
|
||||
|
||||
const showImages = mostRecentItem.children?.some(
|
||||
(child) => child.thumbnail && child.thumbnail !== mostRecentItem.thumbnail
|
||||
const showImages: boolean | undefined = currentItem.children?.some(
|
||||
(child) => child.thumbnail && child.thumbnail !== currentItem.thumbnail
|
||||
);
|
||||
|
||||
const mediaType = this.hass.localize(
|
||||
`ui.components.media-browser.content-type.${mostRecentItem.media_content_type}`
|
||||
`ui.components.media-browser.content-type.${currentItem.media_content_type}`
|
||||
);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="header ${classMap({
|
||||
"no-img": !mostRecentItem.thumbnail,
|
||||
"no-img": !currentItem.thumbnail,
|
||||
})}"
|
||||
>
|
||||
<div class="header-content">
|
||||
${mostRecentItem.thumbnail
|
||||
${currentItem.thumbnail
|
||||
? html`
|
||||
<div
|
||||
class="img"
|
||||
style="background-image: url(${mostRecentItem.thumbnail})"
|
||||
style="background-image: url(${currentItem.thumbnail})"
|
||||
>
|
||||
${this._narrow && mostRecentItem?.can_play
|
||||
${this._narrow && currentItem?.can_play
|
||||
? html`
|
||||
<mwc-fab
|
||||
mini
|
||||
.item=${mostRecentItem}
|
||||
.item=${currentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
@ -153,35 +156,29 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
`
|
||||
: html``}
|
||||
<div class="header-info">
|
||||
${this.hideTitle && (this._narrow || !mostRecentItem.thumbnail)
|
||||
? ""
|
||||
: html`<div class="breadcrumb-overflow">
|
||||
<div class="breadcrumb">
|
||||
${!this.hideBack && previousItem
|
||||
? html`
|
||||
<div
|
||||
class="previous-title"
|
||||
@click=${this.navigateBack}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
|
||||
${previousItem.title}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<h1 class="title">${mostRecentItem.title}</h1>
|
||||
${mediaType
|
||||
? html`<h2 class="subtitle">
|
||||
${mediaType}
|
||||
</h2>`
|
||||
: ""}
|
||||
</div>
|
||||
</div>`}
|
||||
${mostRecentItem?.can_play &&
|
||||
(!mostRecentItem.thumbnail || !this._narrow)
|
||||
<div class="breadcrumb">
|
||||
${previousItem
|
||||
? html`
|
||||
<div class="previous-title" @click=${this.navigateBack}>
|
||||
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
|
||||
${previousItem.title}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<h1 class="title">${currentItem.title}</h1>
|
||||
${mediaType
|
||||
? html`
|
||||
<h2 class="subtitle">
|
||||
${mediaType}
|
||||
</h2>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
${currentItem.can_play && (!currentItem.thumbnail || !this._narrow)
|
||||
? html`
|
||||
<mwc-button
|
||||
raised
|
||||
.item=${mostRecentItem}
|
||||
.item=${currentItem}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
@ -207,73 +204,69 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
class="header_button"
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<ha-svg-icon path=${mdiClose}></ha-svg-icon>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
${mostRecentItem.children?.length
|
||||
${currentItem.children?.length
|
||||
? hasExpandableChildren
|
||||
? html`
|
||||
<div class="children">
|
||||
${mostRecentItem.children?.length
|
||||
? html`
|
||||
${mostRecentItem.children.map(
|
||||
(child) => html`
|
||||
<div
|
||||
class="child"
|
||||
.item=${child}
|
||||
@click=${this._navigateForward}
|
||||
>
|
||||
<div class="ha-card-parent">
|
||||
<ha-card
|
||||
outlined
|
||||
style="background-image: url(${child.thumbnail})"
|
||||
${currentItem.children.map(
|
||||
(child) => html`
|
||||
<div
|
||||
class="child"
|
||||
.item=${child}
|
||||
@click=${this._navigateForward}
|
||||
>
|
||||
<div class="ha-card-parent">
|
||||
<ha-card
|
||||
outlined
|
||||
style="background-image: url(${child.thumbnail})"
|
||||
>
|
||||
${child.can_expand && !child.thumbnail
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${mdiFolder}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
${child.can_play
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
class="play"
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
${child.can_expand && !child.thumbnail
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="folder"
|
||||
.path=${mdiFolder}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
${child.can_play
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
class="play"
|
||||
.item=${child}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.${this.action}-media`
|
||||
)}
|
||||
@click=${this._actionClicked}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="title">${child.title}</div>
|
||||
<div class="type">
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.content-type.${child.media_content_type}`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
<ha-svg-icon
|
||||
.path=${this.action === "play"
|
||||
? mdiPlay
|
||||
: mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="title">${child.title}</div>
|
||||
<div class="type">
|
||||
${this.hass.localize(
|
||||
`ui.components.media-browser.content-type.${child.media_content_type}`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<mwc-list>
|
||||
${mostRecentItem.children.map(
|
||||
${currentItem.children.map(
|
||||
(child) => html`
|
||||
<mwc-list-item
|
||||
@click=${this._actionClicked}
|
||||
@ -353,10 +346,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
private _runAction(item: MediaPlayerItem): void {
|
||||
fireEvent(this, "media-picked", {
|
||||
media_content_id: item.media_content_id,
|
||||
media_content_type: item.media_content_type,
|
||||
});
|
||||
fireEvent(this, "media-picked", { item });
|
||||
}
|
||||
|
||||
private async _navigateForward(ev: MouseEvent): Promise<void> {
|
||||
@ -383,12 +373,15 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
mediaContentId?: string,
|
||||
mediaContentType?: string
|
||||
): Promise<MediaPlayerItem> {
|
||||
const itemData = await browseMediaPlayer(
|
||||
this.hass,
|
||||
this.entityId,
|
||||
!mediaContentId ? undefined : mediaContentId,
|
||||
mediaContentType
|
||||
);
|
||||
const itemData =
|
||||
this.entityId !== BROWSER_SOURCE
|
||||
? await browseMediaPlayer(
|
||||
this.hass,
|
||||
this.entityId,
|
||||
mediaContentId,
|
||||
mediaContentType
|
||||
)
|
||||
: await browseLocalMediaPlayer(this.hass, mediaContentId);
|
||||
|
||||
return itemData;
|
||||
}
|
||||
@ -485,12 +478,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.breadcrumb-overflow {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -716,6 +703,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
|
||||
:host(:not([narrow])[scroll]) .header-info {
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
:host([scroll]) .header-info mwc-button,
|
||||
.no-img .header-info mwc-button {
|
||||
padding-right: 4px;
|
||||
|
@ -20,9 +20,10 @@ export const CONTRAST_RATIO = 4.5;
|
||||
|
||||
export type MediaPlayerBrowseAction = "pick" | "play";
|
||||
|
||||
export const BROWSER_SOURCE = "browser";
|
||||
|
||||
export interface MediaPickedEvent {
|
||||
media_content_id: string;
|
||||
media_content_type: string;
|
||||
item: MediaPlayerItem;
|
||||
}
|
||||
|
||||
export interface MediaPlayerThumbnail {
|
||||
@ -58,6 +59,15 @@ export const browseMediaPlayer = (
|
||||
media_content_type: mediaContentType,
|
||||
});
|
||||
|
||||
export const browseLocalMediaPlayer = (
|
||||
hass: HomeAssistant,
|
||||
mediaContentId?: string
|
||||
): Promise<MediaPlayerItem> =>
|
||||
hass.callWS<MediaPlayerItem>({
|
||||
type: "media_source/browse_media",
|
||||
media_content_id: mediaContentId,
|
||||
});
|
||||
|
||||
export const getCurrentProgress = (stateObj: HassEntity): number => {
|
||||
let progress = stateObj.attributes.media_position;
|
||||
|
||||
|
@ -409,8 +409,8 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
entityId: this.stateObj!.entity_id,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
|
||||
this._playMedia(
|
||||
pickedMedia.media_content_id,
|
||||
pickedMedia.media_content_type
|
||||
pickedMedia.item.media_content_id,
|
||||
pickedMedia.item.media_content_type
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { PolymerElement } from "@polymer/polymer";
|
||||
import {
|
||||
STATE_NOT_RUNNING,
|
||||
STATE_RUNNING,
|
||||
STATE_STARTING,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { customElement, property, PropertyValues } from "lit-element";
|
||||
import { deepActiveElement } from "../common/dom/deep-active-element";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import { CustomPanelInfo } from "../data/panel_custom";
|
||||
import { HomeAssistant, Panels } from "../types";
|
||||
import { removeInitSkeleton } from "../util/init-skeleton";
|
||||
import {
|
||||
@ -8,13 +15,6 @@ import {
|
||||
RouteOptions,
|
||||
RouterOptions,
|
||||
} from "./hass-router-page";
|
||||
import {
|
||||
STATE_STARTING,
|
||||
STATE_NOT_RUNNING,
|
||||
STATE_RUNNING,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { CustomPanelInfo } from "../data/panel_custom";
|
||||
import { deepActiveElement } from "../common/dom/deep-active-element";
|
||||
|
||||
const CACHE_URL_PATHS = ["lovelace", "developer-tools"];
|
||||
const COMPONENTS = {
|
||||
@ -64,6 +64,10 @@ const COMPONENTS = {
|
||||
import(
|
||||
/* webpackChunkName: "panel-shopping-list" */ "../panels/shopping-list/ha-panel-shopping-list"
|
||||
),
|
||||
"media-browser": () =>
|
||||
import(
|
||||
/* webpackChunkName: "panel-media-browser" */ "../panels/media-browser/ha-panel-media-browser"
|
||||
),
|
||||
};
|
||||
|
||||
const getRoutes = (panels: Panels): RouterOptions => {
|
||||
|
@ -667,8 +667,8 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
entityId: this._config!.entity,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
|
||||
this._playMedia(
|
||||
pickedMedia.media_content_id,
|
||||
pickedMedia.media_content_type
|
||||
pickedMedia.item.media_content_id,
|
||||
pickedMedia.item.media_content_type
|
||||
),
|
||||
});
|
||||
}
|
||||
|
151
src/panels/media-browser/ha-panel-media-browser.ts
Normal file
151
src/panels/media-browser/ha-panel-media-browser.ts
Normal file
@ -0,0 +1,151 @@
|
||||
import "@material/mwc-icon-button";
|
||||
import { mdiPlayNetwork } from "@mdi/js";
|
||||
import "@polymer/app-layout/app-header/app-header";
|
||||
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { LocalStorage } from "../../common/decorators/local-storage";
|
||||
import { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/media-player/ha-media-player-browse";
|
||||
import {
|
||||
BROWSER_SOURCE,
|
||||
MediaPickedEvent,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
} from "../../data/media-player";
|
||||
import "../../layouts/ha-app-layout";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showWebBrowserPlayMediaDialog } from "./show-media-player-dialog";
|
||||
import { showSelectMediaPlayerDialog } from "./show-select-media-source-dialog";
|
||||
|
||||
@customElement("ha-panel-media-browser")
|
||||
class PanelMediaBrowser extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public narrow!: boolean;
|
||||
|
||||
// @ts-ignore
|
||||
@LocalStorage("mediaBrowseEntityId", true)
|
||||
private _entityId = BROWSER_SOURCE;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const stateObj = this._entityId
|
||||
? this.hass.states[this._entityId]
|
||||
: undefined;
|
||||
|
||||
const title =
|
||||
this._entityId === BROWSER_SOURCE
|
||||
? `${this.hass.localize("ui.components.media-browser.web-browser")} - `
|
||||
: stateObj?.attributes.friendly_name
|
||||
? `${stateObj?.attributes.friendly_name} - `
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-app-layout>
|
||||
<app-header fixed slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
<div main-title>
|
||||
${title || ""}${this.hass.localize(
|
||||
"ui.components.media-browser.media-player-browser"
|
||||
)}
|
||||
</div>
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.media-browser.choose-player"
|
||||
)}
|
||||
@click=${this._showSelectMediaPlayerDialog}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlayNetwork}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<div class="content">
|
||||
<ha-media-player-browse
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
@media-picked=${this._mediaPicked}
|
||||
></ha-media-player-browse>
|
||||
</div>
|
||||
</ha-app-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
private _showSelectMediaPlayerDialog(): void {
|
||||
showSelectMediaPlayerDialog(this, {
|
||||
mediaSources: this._mediaPlayerEntities,
|
||||
sourceSelectedCallback: (entityId) => {
|
||||
this._entityId = entityId;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _mediaPicked(
|
||||
ev: HASSDomEvent<MediaPickedEvent>
|
||||
): Promise<void> {
|
||||
const item = ev.detail.item;
|
||||
if (this._entityId === BROWSER_SOURCE) {
|
||||
const resolvedUrl: any = await this.hass.callWS({
|
||||
type: "media_source/resolve_media",
|
||||
media_content_id: item.media_content_id,
|
||||
});
|
||||
|
||||
showWebBrowserPlayMediaDialog(this, {
|
||||
sourceUrl: resolvedUrl.url,
|
||||
sourceType: resolvedUrl.mime_type,
|
||||
title: item.title,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass!.callService("media_player", "play_media", {
|
||||
entity_id: this._entityId,
|
||||
media_content_id: item.media_content_id,
|
||||
media_content_type: item.media_content_type,
|
||||
});
|
||||
}
|
||||
|
||||
private get _mediaPlayerEntities() {
|
||||
return Object.values(this.hass!.states).filter((entity) => {
|
||||
if (
|
||||
computeStateDomain(entity) === "media_player" &&
|
||||
supportsFeature(entity, SUPPORT_BROWSE_MEDIA)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-media-player-browse {
|
||||
height: calc(100vh - 84px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-panel-media-browser": PanelMediaBrowser;
|
||||
}
|
||||
}
|
93
src/panels/media-browser/hui-dialog-select-media-player.ts
Normal file
93
src/panels/media-browser/hui-dialog-select-media-player.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import { BROWSER_SOURCE } from "../../data/media-player";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { SelectMediaPlayerDialogParams } from "./show-select-media-source-dialog";
|
||||
|
||||
@customElement("hui-dialog-select-media-player")
|
||||
export class HuiDialogSelectMediaPlayer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
private _params?: SelectMediaPlayerDialogParams;
|
||||
|
||||
public showDialog(params: SelectMediaPlayerDialogParams): void {
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize(`ui.components.media-browser.choose_player`)
|
||||
)}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<paper-listbox
|
||||
attr-for-selected="itemName"
|
||||
@iron-select=${this._selectSource}
|
||||
><paper-item .itemName=${BROWSER_SOURCE}
|
||||
>${this.hass.localize(
|
||||
"ui.components.media-browser.web-browser"
|
||||
)}</paper-item
|
||||
>
|
||||
${this._params.mediaSources.map(
|
||||
(source) => html`
|
||||
<paper-item .itemName=${source.entity_id}
|
||||
>${source.attributes.friendly_name}</paper-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _selectSource(ev: CustomEvent): void {
|
||||
const entityId = ev.detail.item.itemName;
|
||||
this._params!.sourceSelectedCallback(entityId);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0 24px 20px;
|
||||
}
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-select-media-player": HuiDialogSelectMediaPlayer;
|
||||
}
|
||||
}
|
122
src/panels/media-browser/hui-dialog-web-browser-play-media.ts
Normal file
122
src/panels/media-browser/hui-dialog-web-browser-play-media.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-hls-player";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { WebBrowserPlayMediaDialogParams } from "./show-media-player-dialog";
|
||||
|
||||
@customElement("hui-dialog-web-browser-play-media")
|
||||
export class HuiDialogWebBrowserPlayMedia extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
private _params?: WebBrowserPlayMediaDialogParams;
|
||||
|
||||
public showDialog(params: WebBrowserPlayMediaDialogParams): void {
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params || !this._params.sourceType || !this._params.sourceUrl) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const mediaType = this._params.sourceType.split("/", 1)[0];
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._params.title ||
|
||||
this.hass.localize("ui.components.media-browser.media_player")
|
||||
)}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
${mediaType === "audio"
|
||||
? html`
|
||||
<audio controls autoplay>
|
||||
<source
|
||||
src=${this._params.sourceUrl}
|
||||
type=${this._params.sourceType}
|
||||
/>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.audio_not_supported"
|
||||
)}
|
||||
</audio>
|
||||
`
|
||||
: mediaType === "video"
|
||||
? html`
|
||||
<video controls autoplay playsinline>
|
||||
<source
|
||||
src=${this._params.sourceUrl}
|
||||
type=${this._params.sourceType}
|
||||
/>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.video_not_supported"
|
||||
)}
|
||||
</video>
|
||||
`
|
||||
: this._params.sourceType === "application/x-mpegURL"
|
||||
? html`
|
||||
<ha-hls-player
|
||||
controls
|
||||
autoplay
|
||||
playsinline
|
||||
.hass=${this.hass}
|
||||
.url=${this._params.sourceUrl}
|
||||
></ha-hls-player>
|
||||
`
|
||||
: mediaType === "image"
|
||||
? html`<img src=${this._params.sourceUrl} />`
|
||||
: html`${this.hass.localize(
|
||||
"ui.components.media-browser.media_not_supported"
|
||||
)}`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-heading-ink-color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
--mdc-dialog-min-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
video,
|
||||
audio,
|
||||
img {
|
||||
outline: none;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-web-browser-play-media": HuiDialogWebBrowserPlayMedia;
|
||||
}
|
||||
}
|
21
src/panels/media-browser/show-media-player-dialog.ts
Normal file
21
src/panels/media-browser/show-media-player-dialog.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export interface WebBrowserPlayMediaDialogParams {
|
||||
sourceUrl: string;
|
||||
sourceType: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const showWebBrowserPlayMediaDialog = (
|
||||
element: HTMLElement,
|
||||
webBrowserPlayMediaDialogParams: WebBrowserPlayMediaDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "hui-dialog-web-browser-play-media",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "hui-dialog-media-player" */ "./hui-dialog-web-browser-play-media"
|
||||
),
|
||||
dialogParams: webBrowserPlayMediaDialogParams,
|
||||
});
|
||||
};
|
21
src/panels/media-browser/show-select-media-source-dialog.ts
Normal file
21
src/panels/media-browser/show-select-media-source-dialog.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export interface SelectMediaPlayerDialogParams {
|
||||
mediaSources: HassEntity[];
|
||||
sourceSelectedCallback: (entityId: string) => void;
|
||||
}
|
||||
|
||||
export const showSelectMediaPlayerDialog = (
|
||||
element: HTMLElement,
|
||||
selectMediaPlayereDialogParams: SelectMediaPlayerDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "hui-dialog-select-media-player",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "hui-dialog-select-media-player" */ "./hui-dialog-select-media-player"
|
||||
),
|
||||
dialogParams: selectMediaPlayereDialogParams,
|
||||
});
|
||||
};
|
@ -357,8 +357,13 @@
|
||||
"play-media": "Play Media",
|
||||
"pick-media": "Pick Media",
|
||||
"no_items": "No items",
|
||||
"choose-source": "Choose Source",
|
||||
"choose_player": "Choose Player",
|
||||
"media-player-browser": "Media Player Browser",
|
||||
"web-browser": "Web Browser",
|
||||
"media_player": "Media Player",
|
||||
"audio_not_supported": "Your browser does not support the audio element.",
|
||||
"video_not_supported": "Your browser does not support the video element.",
|
||||
"media_not_supported": "The Browser Media Player does not support this type of media",
|
||||
"content-type": {
|
||||
"server": "Server",
|
||||
"library": "Library",
|
||||
|
Loading…
x
Reference in New Issue
Block a user