mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-22 16:56:35 +00:00
Reflect media browser panel state in URL (#11317)
This commit is contained in:
parent
32bbdc194a
commit
09a27a6791
@ -9,32 +9,30 @@ import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-dialog";
|
||||
import "./ha-media-player-browse";
|
||||
import type { MediaPlayerItemId } from "./ha-media-player-browse";
|
||||
import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog";
|
||||
|
||||
@customElement("dialog-media-player-browse")
|
||||
class DialogMediaPlayerBrowse extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _entityId!: string;
|
||||
|
||||
@state() private _mediaContentId?: string;
|
||||
|
||||
@state() private _mediaContentType?: string;
|
||||
|
||||
@state() private _action?: MediaPlayerBrowseAction;
|
||||
@state() private _navigateIds?: MediaPlayerItemId[];
|
||||
|
||||
@state() private _params?: MediaPlayerBrowseDialogParams;
|
||||
|
||||
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";
|
||||
this._navigateIds = [
|
||||
{
|
||||
media_content_id: this._params.mediaContentId,
|
||||
media_content_type: this._params.mediaContentType,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
this._navigateIds = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@ -55,17 +53,21 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
<ha-media-player-browse
|
||||
dialog
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.action=${this._action!}
|
||||
.mediaContentId=${this._mediaContentId}
|
||||
.mediaContentType=${this._mediaContentType}
|
||||
.entityId=${this._params.entityId}
|
||||
.navigateIds=${this._navigateIds}
|
||||
.action=${this._action}
|
||||
@close-dialog=${this.closeDialog}
|
||||
@media-picked=${this._mediaPicked}
|
||||
@media-browsed=${this._mediaBrowsed}
|
||||
></ha-media-player-browse>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _mediaBrowsed(ev) {
|
||||
this._navigateIds = ev.detail.ids;
|
||||
}
|
||||
|
||||
private _mediaPicked(ev: HASSDomEvent<MediaPickedEvent>): void {
|
||||
this._params!.mediaPickedCallback(ev.detail);
|
||||
if (this._action !== "play") {
|
||||
@ -73,6 +75,10 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private get _action(): MediaPlayerBrowseAction {
|
||||
return this._params!.action || "play";
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
|
@ -53,19 +53,21 @@ import "../ha-svg-icon";
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"media-picked": MediaPickedEvent;
|
||||
"media-browsed": { ids: MediaPlayerItemId[]; back?: boolean };
|
||||
}
|
||||
}
|
||||
|
||||
export interface MediaPlayerItemId {
|
||||
media_content_id: string | undefined;
|
||||
media_content_type: string | undefined;
|
||||
}
|
||||
|
||||
@customElement("ha-media-player-browse")
|
||||
export class HaMediaPlayerBrowse extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public entityId!: string;
|
||||
|
||||
@property() public mediaContentId?: string;
|
||||
|
||||
@property() public mediaContentType?: string;
|
||||
|
||||
@property() public action: MediaPlayerBrowseAction = "play";
|
||||
|
||||
@property({ type: Boolean }) public dialog = false;
|
||||
@ -76,11 +78,13 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
@property({ type: Boolean, attribute: "scroll", reflect: true })
|
||||
private _scrolled = false;
|
||||
|
||||
@state() private _loading = false;
|
||||
@property() public navigateIds!: MediaPlayerItemId[];
|
||||
|
||||
@state() private _error?: { message: string; code: string };
|
||||
|
||||
@state() private _mediaPlayerItems: MediaPlayerItem[] = [];
|
||||
@state() private _parentItem?: MediaPlayerItem;
|
||||
|
||||
@state() private _currentItem?: MediaPlayerItem;
|
||||
|
||||
@query(".header") private _header?: HTMLDivElement;
|
||||
|
||||
@ -109,47 +113,18 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public navigateBack() {
|
||||
this._mediaPlayerItems!.pop();
|
||||
const item = this._mediaPlayerItems!.pop();
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
this._navigate(item);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._loading) {
|
||||
if (this._error) {
|
||||
return html`
|
||||
<div class="container">${this._renderError(this._error)}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this._currentItem) {
|
||||
return html`<ha-circular-progress active></ha-circular-progress>`;
|
||||
}
|
||||
|
||||
if (this._error && !this._mediaPlayerItems.length) {
|
||||
if (this.dialog) {
|
||||
this._closeDialogAction();
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.components.media-browser.media_browsing_error"
|
||||
),
|
||||
text: this._renderError(this._error),
|
||||
});
|
||||
} else {
|
||||
return html`
|
||||
<div class="container">${this._renderError(this._error)}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._mediaPlayerItems.length) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const currentItem =
|
||||
this._mediaPlayerItems[this._mediaPlayerItems.length - 1];
|
||||
|
||||
const previousItem: MediaPlayerItem | undefined =
|
||||
this._mediaPlayerItems.length > 1
|
||||
? this._mediaPlayerItems[this._mediaPlayerItems.length - 2]
|
||||
: undefined;
|
||||
const currentItem = this._currentItem;
|
||||
|
||||
const subtitle = this.hass.localize(
|
||||
`ui.components.media-browser.class.${currentItem.media_class}`
|
||||
@ -202,11 +177,11 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
: html``}
|
||||
<div class="header-info">
|
||||
<div class="breadcrumb">
|
||||
${previousItem
|
||||
${this.navigateIds.length > 1
|
||||
? html`
|
||||
<div class="previous-title" @click=${this.navigateBack}>
|
||||
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
|
||||
${previousItem.title}
|
||||
${this._parentItem ? this._parentItem.title : ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
@ -401,49 +376,115 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
this._attachResizeObserver();
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.size > 1 || !changedProps.has("hass")) {
|
||||
return true;
|
||||
}
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
return oldHass === undefined || oldHass.localize !== this.hass.localize;
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (changedProps.has("entityId")) {
|
||||
this._setError(undefined);
|
||||
}
|
||||
if (!changedProps.has("navigateIds")) {
|
||||
return;
|
||||
}
|
||||
const oldNavigateIds = changedProps.get("navigateIds") as
|
||||
| this["navigateIds"]
|
||||
| undefined;
|
||||
|
||||
// We're navigating. Reset the shizzle.
|
||||
this._content?.scrollTo(0, 0);
|
||||
this._scrolled = false;
|
||||
const oldCurrentItem = this._currentItem;
|
||||
const oldParentItem = this._parentItem;
|
||||
this._currentItem = undefined;
|
||||
this._parentItem = undefined;
|
||||
const currentId = this.navigateIds[this.navigateIds.length - 1];
|
||||
const parentId =
|
||||
this.navigateIds.length > 1
|
||||
? this.navigateIds[this.navigateIds.length - 2]
|
||||
: undefined;
|
||||
let currentProm: Promise<MediaPlayerItem> | undefined;
|
||||
let parentProm: Promise<MediaPlayerItem> | undefined;
|
||||
|
||||
// See if we can take loading shortcuts if navigating to parent or child
|
||||
if (
|
||||
// Check if we navigated to a child
|
||||
oldNavigateIds &&
|
||||
this.navigateIds.length > oldNavigateIds.length &&
|
||||
oldNavigateIds.every((oldVal, idx) => {
|
||||
const curVal = this.navigateIds[idx];
|
||||
return (
|
||||
curVal.media_content_id === oldVal.media_content_id &&
|
||||
curVal.media_content_type === oldVal.media_content_type
|
||||
);
|
||||
})
|
||||
) {
|
||||
parentProm = Promise.resolve(oldCurrentItem!);
|
||||
} else if (
|
||||
// Check if we navigated to a parent
|
||||
oldNavigateIds &&
|
||||
this.navigateIds.length < oldNavigateIds.length &&
|
||||
this.navigateIds.every((curVal, idx) => {
|
||||
const oldVal = oldNavigateIds[idx];
|
||||
return (
|
||||
curVal.media_content_id === oldVal.media_content_id &&
|
||||
curVal.media_content_type === oldVal.media_content_type
|
||||
);
|
||||
})
|
||||
) {
|
||||
currentProm = Promise.resolve(oldParentItem!);
|
||||
}
|
||||
// Fetch current
|
||||
if (!currentProm) {
|
||||
currentProm = this._fetchData(
|
||||
this.entityId,
|
||||
currentId.media_content_id,
|
||||
currentId.media_content_type
|
||||
);
|
||||
}
|
||||
currentProm.then(
|
||||
(item) => {
|
||||
this._currentItem = item;
|
||||
},
|
||||
(err) => this._setError(err)
|
||||
);
|
||||
// Fetch parent
|
||||
if (!parentProm && parentId !== undefined) {
|
||||
parentProm = this._fetchData(
|
||||
this.entityId,
|
||||
parentId.media_content_id,
|
||||
parentId.media_content_type
|
||||
);
|
||||
}
|
||||
if (parentProm) {
|
||||
parentProm.then((parent) => {
|
||||
this._parentItem = parent;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (
|
||||
changedProps.has("_mediaPlayerItems") &&
|
||||
this._mediaPlayerItems.length
|
||||
) {
|
||||
if (changedProps.has("_scrolled")) {
|
||||
this._animateHeaderHeight();
|
||||
} else if (changedProps.has("_currentItem")) {
|
||||
this._setHeaderHeight();
|
||||
this._attachIntersectionObserver();
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
changedProps.get("_scrolled") !== undefined &&
|
||||
this._mediaPlayerItems.length
|
||||
) {
|
||||
this._animateHeaderHeight();
|
||||
}
|
||||
|
||||
if (
|
||||
!changedProps.has("entityId") &&
|
||||
!changedProps.has("mediaContentId") &&
|
||||
!changedProps.has("mediaContentType") &&
|
||||
!changedProps.has("action")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (changedProps.has("entityId")) {
|
||||
this._error = undefined;
|
||||
this._mediaPlayerItems = [];
|
||||
}
|
||||
|
||||
this._fetchData(this.mediaContentId, this.mediaContentType)
|
||||
.then((itemData) => {
|
||||
if (!itemData) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._mediaPlayerItems = [itemData];
|
||||
})
|
||||
.catch((err) => {
|
||||
this._error = err;
|
||||
});
|
||||
private navigateBack() {
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: this.navigateIds.slice(0, -1),
|
||||
back: true,
|
||||
});
|
||||
}
|
||||
|
||||
private async _setHeaderHeight() {
|
||||
@ -497,54 +538,19 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._navigate(item);
|
||||
}
|
||||
|
||||
private async _navigate(item: MediaPlayerItem) {
|
||||
this._error = undefined;
|
||||
|
||||
let itemData: MediaPlayerItem;
|
||||
|
||||
try {
|
||||
itemData = await this._fetchData(
|
||||
item.media_content_id,
|
||||
item.media_content_type
|
||||
);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.components.media-browser.media_browsing_error"
|
||||
),
|
||||
text: this._renderError(err),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._content?.scrollTo(0, 0);
|
||||
this._scrolled = false;
|
||||
this._mediaPlayerItems = [...this._mediaPlayerItems, itemData];
|
||||
fireEvent(this, "media-browsed", {
|
||||
ids: [...this.navigateIds, item],
|
||||
});
|
||||
}
|
||||
|
||||
private async _fetchData(
|
||||
entityId: string,
|
||||
mediaContentId?: string,
|
||||
mediaContentType?: string
|
||||
): Promise<MediaPlayerItem> {
|
||||
this._loading = true;
|
||||
let itemData: any;
|
||||
try {
|
||||
itemData =
|
||||
this.entityId !== BROWSER_PLAYER
|
||||
? await browseMediaPlayer(
|
||||
this.hass,
|
||||
this.entityId,
|
||||
mediaContentId,
|
||||
mediaContentType
|
||||
)
|
||||
: await browseLocalMediaPlayer(this.hass, mediaContentId);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
return itemData;
|
||||
return entityId !== BROWSER_PLAYER
|
||||
? browseMediaPlayer(this.hass, entityId, mediaContentId, mediaContentType)
|
||||
: browseLocalMediaPlayer(this.hass, mediaContentId);
|
||||
}
|
||||
|
||||
private _measureCard(): void {
|
||||
@ -576,7 +582,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
* Load thumbnails for images on demand as they become visible.
|
||||
*/
|
||||
private async _attachIntersectionObserver(): Promise<void> {
|
||||
if (!this._thumbnails) {
|
||||
if (!("IntersectionObserver" in window) || !this._thumbnails) {
|
||||
return;
|
||||
}
|
||||
if (!this._intersectionObserver) {
|
||||
@ -605,15 +611,34 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
);
|
||||
}
|
||||
const observer = this._intersectionObserver!;
|
||||
this._thumbnails.forEach((thumbnailCard) => {
|
||||
for (const thumbnailCard of this._thumbnails) {
|
||||
observer.observe(thumbnailCard);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _closeDialogAction(): void {
|
||||
fireEvent(this, "close-dialog");
|
||||
}
|
||||
|
||||
private _setError(error: any) {
|
||||
if (!this.dialog) {
|
||||
this._error = error;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._closeDialogAction();
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.components.media-browser.media_browsing_error"
|
||||
),
|
||||
text: this._renderError(error),
|
||||
});
|
||||
}
|
||||
|
||||
private _renderError(err: { message: string; code: string }) {
|
||||
if (err.message === "Media directory does not exist.") {
|
||||
return html`
|
||||
|
@ -21,13 +21,11 @@ import "./home-assistant-main";
|
||||
|
||||
const useHash = __DEMO__;
|
||||
const curPath = () =>
|
||||
window.decodeURIComponent(
|
||||
useHash ? location.hash.substr(1) : location.pathname
|
||||
);
|
||||
useHash ? location.hash.substring(1) : location.pathname;
|
||||
|
||||
const panelUrl = (path: string) => {
|
||||
const dividerPos = path.indexOf("/", 1);
|
||||
return dividerPos === -1 ? path.substr(1) : path.substr(1, dividerPos - 1);
|
||||
return dividerPos === -1 ? path.substring(1) : path.substring(1, dividerPos);
|
||||
};
|
||||
|
||||
@customElement("home-assistant")
|
||||
|
@ -16,6 +16,7 @@ import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/media-player/ha-media-player-browse";
|
||||
import type { MediaPlayerItemId } from "../../components/media-player/ha-media-player-browse";
|
||||
import {
|
||||
BROWSER_PLAYER,
|
||||
MediaPickedEvent,
|
||||
@ -36,8 +37,14 @@ class PanelMediaBrowser extends LitElement {
|
||||
|
||||
@property() public route!: Route;
|
||||
|
||||
// @ts-ignore
|
||||
@LocalStorage("mediaBrowseEntityId", true)
|
||||
private _navigateIds: MediaPlayerItemId[] = [
|
||||
{
|
||||
media_content_id: undefined,
|
||||
media_content_type: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
@LocalStorage("mediaBrowseEntityId")
|
||||
private _entityId = BROWSER_PLAYER;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@ -77,15 +84,17 @@ class PanelMediaBrowser extends LitElement {
|
||||
<ha-media-player-browse
|
||||
.hass=${this.hass}
|
||||
.entityId=${this._entityId}
|
||||
.navigateIds=${this._navigateIds}
|
||||
@media-picked=${this._mediaPicked}
|
||||
@media-browsed=${this._mediaBrowsed}
|
||||
></ha-media-player-browse>
|
||||
</div>
|
||||
</ha-app-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
public updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!changedProps.has("route")) {
|
||||
return;
|
||||
@ -96,10 +105,28 @@ class PanelMediaBrowser extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const routePlayer = this.route.path.substring(1).split("/")[0];
|
||||
const [routePlayer, ...navigateIdsEncoded] = this.route.path
|
||||
.substring(1)
|
||||
.split("/");
|
||||
|
||||
if (routePlayer !== this._entityId) {
|
||||
this._entityId = routePlayer;
|
||||
}
|
||||
|
||||
this._navigateIds = [
|
||||
{
|
||||
media_content_type: undefined,
|
||||
media_content_id: undefined,
|
||||
},
|
||||
...navigateIdsEncoded.map((navigateId) => {
|
||||
const [media_content_type, media_content_id] =
|
||||
decodeURIComponent(navigateId).split(",");
|
||||
return {
|
||||
media_content_type,
|
||||
media_content_id,
|
||||
};
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private _showSelectMediaPlayerDialog(): void {
|
||||
@ -111,6 +138,22 @@ class PanelMediaBrowser extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _mediaBrowsed(ev) {
|
||||
if (ev.detail.back) {
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
let path = "";
|
||||
for (const item of ev.detail.ids.slice(1)) {
|
||||
path +=
|
||||
"/" +
|
||||
encodeURIComponent(
|
||||
`${item.media_content_type},${item.media_content_id}`
|
||||
);
|
||||
}
|
||||
navigate(`/media-browser/${this._entityId}${path}`);
|
||||
}
|
||||
|
||||
private async _mediaPicked(
|
||||
ev: HASSDomEvent<MediaPickedEvent>
|
||||
): Promise<void> {
|
||||
|
Loading…
x
Reference in New Issue
Block a user