Reflect media browser panel state in URL (#11317)

This commit is contained in:
Paulus Schoutsen 2022-01-17 07:40:51 -08:00 committed by GitHub
parent 32bbdc194a
commit 09a27a6791
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 222 additions and 150 deletions

View File

@ -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,

View File

@ -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`

View File

@ -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")

View File

@ -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> {