mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 01:06:35 +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,
|
CSSResult,
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
import { supportsFeature } from "../common/entity/supports-feature";
|
import { supportsFeature } from "../common/entity/supports-feature";
|
||||||
import { nextRender } from "../common/util/render-status";
|
|
||||||
import { getExternalConfig } from "../external_app/external_config";
|
|
||||||
import {
|
import {
|
||||||
CAMERA_SUPPORT_STREAM,
|
CAMERA_SUPPORT_STREAM,
|
||||||
computeMJPEGStreamUrl,
|
computeMJPEGStreamUrl,
|
||||||
fetchStreamUrl,
|
fetchStreamUrl,
|
||||||
} from "../data/camera";
|
} from "../data/camera";
|
||||||
import { CameraEntity, HomeAssistant } from "../types";
|
import { CameraEntity, HomeAssistant } from "../types";
|
||||||
|
import "./ha-hls-player";
|
||||||
type HLSModule = typeof import("hls.js");
|
|
||||||
|
|
||||||
@customElement("ha-camera-stream")
|
@customElement("ha-camera-stream")
|
||||||
class HaCameraStream extends LitElement {
|
class HaCameraStream extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property() public stateObj?: CameraEntity;
|
@property({ attribute: false }) public stateObj?: CameraEntity;
|
||||||
|
|
||||||
@property({ type: Boolean }) public showControls = false;
|
@property({ type: Boolean }) public showControls = false;
|
||||||
|
|
||||||
@internalProperty() private _attached = false;
|
|
||||||
|
|
||||||
// We keep track if we should force MJPEG with a string
|
// We keep track if we should force MJPEG with a string
|
||||||
// that way it automatically resets if we change entity.
|
// that way it automatically resets if we change entity.
|
||||||
@internalProperty() private _forceMJPEG: string | undefined = undefined;
|
@internalProperty() private _forceMJPEG?: string;
|
||||||
|
|
||||||
private _hlsPolyfillInstance?: Hls;
|
@internalProperty() private _url?: string;
|
||||||
|
|
||||||
private _useExoPlayer = false;
|
|
||||||
|
|
||||||
public connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this._attached = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this._attached = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.stateObj || !this._attached) {
|
if (!this.stateObj || (!this._forceMJPEG && !this._url)) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,50 +53,22 @@ class HaCameraStream extends LitElement {
|
|||||||
/>
|
/>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<video
|
<ha-hls-player
|
||||||
autoplay
|
autoplay
|
||||||
muted
|
muted
|
||||||
playsinline
|
playsinline
|
||||||
?controls=${this.showControls}
|
?controls=${this.showControls}
|
||||||
@loadeddata=${this._elementResized}
|
.hass=${this.hass}
|
||||||
></video>
|
.url=${this._url!}
|
||||||
|
></ha-hls-player>
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues): void {
|
||||||
super.updated(changedProps);
|
if (changedProps.has("stateObj")) {
|
||||||
|
this._forceMJPEG = undefined;
|
||||||
const stateObjChanged = changedProps.has("stateObj");
|
this._getStreamUrl();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,136 +80,35 @@ class HaCameraStream extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _videoEl(): HTMLVideoElement {
|
private async _getStreamUrl(): Promise<void> {
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { url } = await fetchStreamUrl(
|
const { url } = await fetchStreamUrl(
|
||||||
this.hass!,
|
this.hass!,
|
||||||
this.stateObj!.entity_id
|
this.stateObj!.entity_id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this._useExoPlayer) {
|
this._url = url;
|
||||||
this._renderHLSExoPlayer(url);
|
|
||||||
} else if (hls.isSupported()) {
|
|
||||||
this._renderHLSPolyfill(videoEl, hls, url);
|
|
||||||
} else {
|
|
||||||
this._renderHLSNative(videoEl, url);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Fails if we were unable to get a stream
|
// Fails if we were unable to get a stream
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
this._forceMJPEG = this.stateObj!.entity_id;
|
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() {
|
private _elementResized() {
|
||||||
fireEvent(this, "iron-resize");
|
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 {
|
static get styles(): CSSResult {
|
||||||
return css`
|
return css`
|
||||||
:host,
|
:host,
|
||||||
img,
|
img {
|
||||||
video {
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
img,
|
img {
|
||||||
video {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -10,7 +10,7 @@ import "./ha-icon-button";
|
|||||||
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
|
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
|
||||||
|
|
||||||
export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
|
export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
|
||||||
${title}
|
<span class="header_title">${title}</span>
|
||||||
<mwc-icon-button
|
<mwc-icon-button
|
||||||
aria-label=${hass.localize("ui.dialogs.generic.close")}
|
aria-label=${hass.localize("ui.dialogs.generic.close")}
|
||||||
dialogAction="close"
|
dialogAction="close"
|
||||||
@ -77,10 +77,17 @@ export class HaDialog extends MwcDialog {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
.header_title {
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
[dir="rtl"].header_button {
|
[dir="rtl"].header_button {
|
||||||
right: auto;
|
right: auto;
|
||||||
left: 16px;
|
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,
|
property,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { HASSDomEvent } from "../../common/dom/fire_event";
|
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
|
||||||
import type {
|
import type {
|
||||||
MediaPickedEvent,
|
MediaPickedEvent,
|
||||||
MediaPlayerBrowseAction,
|
MediaPlayerBrowseAction,
|
||||||
@ -33,16 +33,17 @@ class DialogMediaPlayerBrowse extends LitElement {
|
|||||||
|
|
||||||
@internalProperty() private _params?: MediaPlayerBrowseDialogParams;
|
@internalProperty() private _params?: MediaPlayerBrowseDialogParams;
|
||||||
|
|
||||||
public async showDialog(
|
public showDialog(params: MediaPlayerBrowseDialogParams): void {
|
||||||
params: MediaPlayerBrowseDialogParams
|
|
||||||
): Promise<void> {
|
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._entityId = this._params.entityId;
|
this._entityId = this._params.entityId;
|
||||||
this._mediaContentId = this._params.mediaContentId;
|
this._mediaContentId = this._params.mediaContentId;
|
||||||
this._mediaContentType = this._params.mediaContentType;
|
this._mediaContentType = this._params.mediaContentType;
|
||||||
this._action = this._params.action || "play";
|
this._action = this._params.action || "play";
|
||||||
|
}
|
||||||
|
|
||||||
await this.updateComplete;
|
public closeDialog() {
|
||||||
|
this._params = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", {dialog: this.localName});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
@ -57,7 +58,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
|||||||
escapeKeyAction
|
escapeKeyAction
|
||||||
hideActions
|
hideActions
|
||||||
flexContent
|
flexContent
|
||||||
@closed=${this._closeDialog}
|
@closed=${this.closeDialog}
|
||||||
>
|
>
|
||||||
<ha-media-player-browse
|
<ha-media-player-browse
|
||||||
dialog
|
dialog
|
||||||
@ -66,21 +67,17 @@ class DialogMediaPlayerBrowse extends LitElement {
|
|||||||
.action=${this._action!}
|
.action=${this._action!}
|
||||||
.mediaContentId=${this._mediaContentId}
|
.mediaContentId=${this._mediaContentId}
|
||||||
.mediaContentType=${this._mediaContentType}
|
.mediaContentType=${this._mediaContentType}
|
||||||
@close-dialog=${this._closeDialog}
|
@close-dialog=${this.closeDialog}
|
||||||
@media-picked=${this._mediaPicked}
|
@media-picked=${this._mediaPicked}
|
||||||
></ha-media-player-browse>
|
></ha-media-player-browse>
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _closeDialog() {
|
|
||||||
this._params = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _mediaPicked(ev: HASSDomEvent<MediaPickedEvent>): void {
|
private _mediaPicked(ev: HASSDomEvent<MediaPickedEvent>): void {
|
||||||
this._params!.mediaPickedCallback(ev.detail);
|
this._params!.mediaPickedCallback(ev.detail);
|
||||||
if (this._action !== "play") {
|
if (this._action !== "play") {
|
||||||
this._closeDialog();
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,14 +90,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
|||||||
--dialog-content-padding: 0;
|
--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) {
|
@media (min-width: 800px) {
|
||||||
ha-dialog {
|
ha-dialog {
|
||||||
--mdc-dialog-max-width: 800px;
|
--mdc-dialog-max-width: 800px;
|
||||||
|
@ -22,7 +22,13 @@ import memoizeOne from "memoize-one";
|
|||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||||
import { debounce } from "../../common/util/debounce";
|
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 type { MediaPlayerItem } from "../../data/media-player";
|
||||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||||
import { haStyle } from "../../resources/styles";
|
import { haStyle } from "../../resources/styles";
|
||||||
@ -50,11 +56,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
|
|
||||||
@property() public mediaContentType?: string;
|
@property() public mediaContentType?: string;
|
||||||
|
|
||||||
@property() public action: "pick" | "play" = "play";
|
@property() public action: MediaPlayerBrowseAction = "play";
|
||||||
|
|
||||||
@property({ type: Boolean }) public hideBack = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public hideTitle = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public dialog = false;
|
@property({ type: Boolean }) public dialog = false;
|
||||||
|
|
||||||
@ -88,52 +90,53 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this._mediaPlayerItems.length) {
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._loading) {
|
if (this._loading) {
|
||||||
return html`<ha-circular-progress active></ha-circular-progress>`;
|
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
|
this._mediaPlayerItems.length - 1
|
||||||
];
|
];
|
||||||
const previousItem =
|
|
||||||
|
const previousItem: MediaPlayerItem | undefined =
|
||||||
this._mediaPlayerItems.length > 1
|
this._mediaPlayerItems.length > 1
|
||||||
? this._mediaPlayerItems[this._mediaPlayerItems.length - 2]
|
? this._mediaPlayerItems[this._mediaPlayerItems.length - 2]
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const hasExpandableChildren:
|
const hasExpandableChildren:
|
||||||
| MediaPlayerItem
|
| MediaPlayerItem
|
||||||
| undefined = this._hasExpandableChildren(mostRecentItem.children);
|
| undefined = this._hasExpandableChildren(currentItem.children);
|
||||||
|
|
||||||
const showImages = mostRecentItem.children?.some(
|
const showImages: boolean | undefined = currentItem.children?.some(
|
||||||
(child) => child.thumbnail && child.thumbnail !== mostRecentItem.thumbnail
|
(child) => child.thumbnail && child.thumbnail !== currentItem.thumbnail
|
||||||
);
|
);
|
||||||
|
|
||||||
const mediaType = this.hass.localize(
|
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`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="header ${classMap({
|
class="header ${classMap({
|
||||||
"no-img": !mostRecentItem.thumbnail,
|
"no-img": !currentItem.thumbnail,
|
||||||
})}"
|
})}"
|
||||||
>
|
>
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
${mostRecentItem.thumbnail
|
${currentItem.thumbnail
|
||||||
? html`
|
? html`
|
||||||
<div
|
<div
|
||||||
class="img"
|
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`
|
? html`
|
||||||
<mwc-fab
|
<mwc-fab
|
||||||
mini
|
mini
|
||||||
.item=${mostRecentItem}
|
.item=${currentItem}
|
||||||
@click=${this._actionClicked}
|
@click=${this._actionClicked}
|
||||||
>
|
>
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
@ -153,35 +156,29 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
`
|
`
|
||||||
: html``}
|
: html``}
|
||||||
<div class="header-info">
|
<div class="header-info">
|
||||||
${this.hideTitle && (this._narrow || !mostRecentItem.thumbnail)
|
<div class="breadcrumb">
|
||||||
? ""
|
${previousItem
|
||||||
: html`<div class="breadcrumb-overflow">
|
? html`
|
||||||
<div class="breadcrumb">
|
<div class="previous-title" @click=${this.navigateBack}>
|
||||||
${!this.hideBack && previousItem
|
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
|
||||||
? html`
|
${previousItem.title}
|
||||||
<div
|
</div>
|
||||||
class="previous-title"
|
`
|
||||||
@click=${this.navigateBack}
|
: ""}
|
||||||
>
|
<h1 class="title">${currentItem.title}</h1>
|
||||||
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
|
${mediaType
|
||||||
${previousItem.title}
|
? html`
|
||||||
</div>
|
<h2 class="subtitle">
|
||||||
`
|
${mediaType}
|
||||||
: ""}
|
</h2>
|
||||||
<h1 class="title">${mostRecentItem.title}</h1>
|
`
|
||||||
${mediaType
|
: ""}
|
||||||
? html`<h2 class="subtitle">
|
</div>
|
||||||
${mediaType}
|
${currentItem.can_play && (!currentItem.thumbnail || !this._narrow)
|
||||||
</h2>`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
</div>`}
|
|
||||||
${mostRecentItem?.can_play &&
|
|
||||||
(!mostRecentItem.thumbnail || !this._narrow)
|
|
||||||
? html`
|
? html`
|
||||||
<mwc-button
|
<mwc-button
|
||||||
raised
|
raised
|
||||||
.item=${mostRecentItem}
|
.item=${currentItem}
|
||||||
@click=${this._actionClicked}
|
@click=${this._actionClicked}
|
||||||
>
|
>
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
@ -207,73 +204,69 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
class="header_button"
|
class="header_button"
|
||||||
dir=${computeRTLDirection(this.hass)}
|
dir=${computeRTLDirection(this.hass)}
|
||||||
>
|
>
|
||||||
<ha-svg-icon path=${mdiClose}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||||
</mwc-icon-button>
|
</mwc-icon-button>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
${mostRecentItem.children?.length
|
${currentItem.children?.length
|
||||||
? hasExpandableChildren
|
? hasExpandableChildren
|
||||||
? html`
|
? html`
|
||||||
<div class="children">
|
<div class="children">
|
||||||
${mostRecentItem.children?.length
|
${currentItem.children.map(
|
||||||
? html`
|
(child) => html`
|
||||||
${mostRecentItem.children.map(
|
<div
|
||||||
(child) => html`
|
class="child"
|
||||||
<div
|
.item=${child}
|
||||||
class="child"
|
@click=${this._navigateForward}
|
||||||
.item=${child}
|
>
|
||||||
@click=${this._navigateForward}
|
<div class="ha-card-parent">
|
||||||
>
|
<ha-card
|
||||||
<div class="ha-card-parent">
|
outlined
|
||||||
<ha-card
|
style="background-image: url(${child.thumbnail})"
|
||||||
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
|
<ha-svg-icon
|
||||||
? html`
|
.path=${this.action === "play"
|
||||||
<ha-svg-icon
|
? mdiPlay
|
||||||
class="folder"
|
: mdiPlus}
|
||||||
.path=${mdiFolder}
|
></ha-svg-icon>
|
||||||
></ha-svg-icon>
|
</mwc-icon-button>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
</ha-card>
|
</div>
|
||||||
${child.can_play
|
<div class="title">${child.title}</div>
|
||||||
? html`
|
<div class="type">
|
||||||
<mwc-icon-button
|
${this.hass.localize(
|
||||||
class="play"
|
`ui.components.media-browser.content-type.${child.media_content_type}`
|
||||||
.item=${child}
|
)}
|
||||||
.label=${this.hass.localize(
|
</div>
|
||||||
`ui.components.media-browser.${this.action}-media`
|
</div>
|
||||||
)}
|
`
|
||||||
@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>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<mwc-list>
|
<mwc-list>
|
||||||
${mostRecentItem.children.map(
|
${currentItem.children.map(
|
||||||
(child) => html`
|
(child) => html`
|
||||||
<mwc-list-item
|
<mwc-list-item
|
||||||
@click=${this._actionClicked}
|
@click=${this._actionClicked}
|
||||||
@ -353,10 +346,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _runAction(item: MediaPlayerItem): void {
|
private _runAction(item: MediaPlayerItem): void {
|
||||||
fireEvent(this, "media-picked", {
|
fireEvent(this, "media-picked", { item });
|
||||||
media_content_id: item.media_content_id,
|
|
||||||
media_content_type: item.media_content_type,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _navigateForward(ev: MouseEvent): Promise<void> {
|
private async _navigateForward(ev: MouseEvent): Promise<void> {
|
||||||
@ -383,12 +373,15 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
mediaContentId?: string,
|
mediaContentId?: string,
|
||||||
mediaContentType?: string
|
mediaContentType?: string
|
||||||
): Promise<MediaPlayerItem> {
|
): Promise<MediaPlayerItem> {
|
||||||
const itemData = await browseMediaPlayer(
|
const itemData =
|
||||||
this.hass,
|
this.entityId !== BROWSER_SOURCE
|
||||||
this.entityId,
|
? await browseMediaPlayer(
|
||||||
!mediaContentId ? undefined : mediaContentId,
|
this.hass,
|
||||||
mediaContentType
|
this.entityId,
|
||||||
);
|
mediaContentId,
|
||||||
|
mediaContentType
|
||||||
|
)
|
||||||
|
: await browseLocalMediaPlayer(this.hass, mediaContentId);
|
||||||
|
|
||||||
return itemData;
|
return itemData;
|
||||||
}
|
}
|
||||||
@ -485,12 +478,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-overflow {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -716,6 +703,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
-webkit-line-clamp: 1;
|
-webkit-line-clamp: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host(:not([narrow])[scroll]) .header-info {
|
||||||
|
height: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
:host([scroll]) .header-info mwc-button,
|
:host([scroll]) .header-info mwc-button,
|
||||||
.no-img .header-info mwc-button {
|
.no-img .header-info mwc-button {
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
|
@ -20,9 +20,10 @@ export const CONTRAST_RATIO = 4.5;
|
|||||||
|
|
||||||
export type MediaPlayerBrowseAction = "pick" | "play";
|
export type MediaPlayerBrowseAction = "pick" | "play";
|
||||||
|
|
||||||
|
export const BROWSER_SOURCE = "browser";
|
||||||
|
|
||||||
export interface MediaPickedEvent {
|
export interface MediaPickedEvent {
|
||||||
media_content_id: string;
|
item: MediaPlayerItem;
|
||||||
media_content_type: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MediaPlayerThumbnail {
|
export interface MediaPlayerThumbnail {
|
||||||
@ -58,6 +59,15 @@ export const browseMediaPlayer = (
|
|||||||
media_content_type: mediaContentType,
|
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 => {
|
export const getCurrentProgress = (stateObj: HassEntity): number => {
|
||||||
let progress = stateObj.attributes.media_position;
|
let progress = stateObj.attributes.media_position;
|
||||||
|
|
||||||
|
@ -409,8 +409,8 @@ class MoreInfoMediaPlayer extends LitElement {
|
|||||||
entityId: this.stateObj!.entity_id,
|
entityId: this.stateObj!.entity_id,
|
||||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
|
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
|
||||||
this._playMedia(
|
this._playMedia(
|
||||||
pickedMedia.media_content_id,
|
pickedMedia.item.media_content_id,
|
||||||
pickedMedia.media_content_type
|
pickedMedia.item.media_content_type
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
import { PolymerElement } from "@polymer/polymer";
|
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 { customElement, property, PropertyValues } from "lit-element";
|
||||||
|
import { deepActiveElement } from "../common/dom/deep-active-element";
|
||||||
import { deepEqual } from "../common/util/deep-equal";
|
import { deepEqual } from "../common/util/deep-equal";
|
||||||
|
import { CustomPanelInfo } from "../data/panel_custom";
|
||||||
import { HomeAssistant, Panels } from "../types";
|
import { HomeAssistant, Panels } from "../types";
|
||||||
import { removeInitSkeleton } from "../util/init-skeleton";
|
import { removeInitSkeleton } from "../util/init-skeleton";
|
||||||
import {
|
import {
|
||||||
@ -8,13 +15,6 @@ import {
|
|||||||
RouteOptions,
|
RouteOptions,
|
||||||
RouterOptions,
|
RouterOptions,
|
||||||
} from "./hass-router-page";
|
} 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 CACHE_URL_PATHS = ["lovelace", "developer-tools"];
|
||||||
const COMPONENTS = {
|
const COMPONENTS = {
|
||||||
@ -64,6 +64,10 @@ const COMPONENTS = {
|
|||||||
import(
|
import(
|
||||||
/* webpackChunkName: "panel-shopping-list" */ "../panels/shopping-list/ha-panel-shopping-list"
|
/* 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 => {
|
const getRoutes = (panels: Panels): RouterOptions => {
|
||||||
|
@ -667,8 +667,8 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
|||||||
entityId: this._config!.entity,
|
entityId: this._config!.entity,
|
||||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
|
mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
|
||||||
this._playMedia(
|
this._playMedia(
|
||||||
pickedMedia.media_content_id,
|
pickedMedia.item.media_content_id,
|
||||||
pickedMedia.media_content_type
|
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",
|
"play-media": "Play Media",
|
||||||
"pick-media": "Pick Media",
|
"pick-media": "Pick Media",
|
||||||
"no_items": "No items",
|
"no_items": "No items",
|
||||||
"choose-source": "Choose Source",
|
"choose_player": "Choose Player",
|
||||||
"media-player-browser": "Media Player Browser",
|
"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": {
|
"content-type": {
|
||||||
"server": "Server",
|
"server": "Server",
|
||||||
"library": "Library",
|
"library": "Library",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user