mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-25 13:57:21 +00:00
1120 lines
35 KiB
TypeScript
1120 lines
35 KiB
TypeScript
import "@material/mwc-button/mwc-button";
|
|
import "@material/mwc-list/mwc-list";
|
|
import "@material/mwc-list/mwc-list-item";
|
|
import { mdiPlay, mdiPlus } from "@mdi/js";
|
|
import "@polymer/paper-item/paper-item";
|
|
import "@polymer/paper-listbox/paper-listbox";
|
|
import "@polymer/paper-tooltip/paper-tooltip";
|
|
import {
|
|
css,
|
|
CSSResultGroup,
|
|
html,
|
|
LitElement,
|
|
PropertyValues,
|
|
TemplateResult,
|
|
} from "lit";
|
|
import {
|
|
customElement,
|
|
eventOptions,
|
|
property,
|
|
query,
|
|
queryAll,
|
|
state,
|
|
} from "lit/decorators";
|
|
import { classMap } from "lit/directives/class-map";
|
|
import { ifDefined } from "lit/directives/if-defined";
|
|
import { styleMap } from "lit/directives/style-map";
|
|
import { fireEvent } from "../../common/dom/fire_event";
|
|
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
|
import { debounce } from "../../common/util/debounce";
|
|
import { getSignedPath } from "../../data/auth";
|
|
import type { MediaPlayerItem } from "../../data/media-player";
|
|
import {
|
|
browseMediaPlayer,
|
|
BROWSER_PLAYER,
|
|
MediaClassBrowserSettings,
|
|
MediaPickedEvent,
|
|
MediaPlayerBrowseAction,
|
|
} from "../../data/media-player";
|
|
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
|
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
|
import { haStyle } from "../../resources/styles";
|
|
import type { HomeAssistant } from "../../types";
|
|
import { documentationUrl } from "../../util/documentation-url";
|
|
import "../entity/ha-entity-picker";
|
|
import "../ha-button-menu";
|
|
import "../ha-card";
|
|
import type { HaCard } from "../ha-card";
|
|
import "../ha-circular-progress";
|
|
import "../ha-icon-button";
|
|
import "../ha-svg-icon";
|
|
import "../ha-fab";
|
|
import { browseLocalMediaPlayer } from "../../data/media_source";
|
|
|
|
declare global {
|
|
interface HASSDomEvents {
|
|
"media-picked": MediaPickedEvent;
|
|
"media-browsed": { ids: MediaPlayerItemId[]; current?: MediaPlayerItem };
|
|
}
|
|
}
|
|
|
|
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 action: MediaPlayerBrowseAction = "play";
|
|
|
|
@property({ type: Boolean }) public dialog = false;
|
|
|
|
@property() public navigateIds!: MediaPlayerItemId[];
|
|
|
|
@property({ type: Boolean, attribute: "narrow", reflect: true })
|
|
// @ts-ignore
|
|
private _narrow = false;
|
|
|
|
@property({ type: Boolean, attribute: "scroll", reflect: true })
|
|
private _scrolled = false;
|
|
|
|
@state() private _error?: { message: string; code: string };
|
|
|
|
@state() private _parentItem?: MediaPlayerItem;
|
|
|
|
@state() private _currentItem?: MediaPlayerItem;
|
|
|
|
@query(".header") private _header?: HTMLDivElement;
|
|
|
|
@query(".content") private _content?: HTMLDivElement;
|
|
|
|
@queryAll(".lazythumbnail") private _thumbnails?: HaCard[];
|
|
|
|
private _headerOffsetHeight = 0;
|
|
|
|
private _resizeObserver?: ResizeObserver;
|
|
|
|
// @ts-ignore
|
|
private _intersectionObserver?: IntersectionObserver;
|
|
|
|
public connectedCallback(): void {
|
|
super.connectedCallback();
|
|
this.updateComplete.then(() => this._attachResizeObserver());
|
|
}
|
|
|
|
public disconnectedCallback(): void {
|
|
if (this._resizeObserver) {
|
|
this._resizeObserver.disconnect();
|
|
}
|
|
if (this._intersectionObserver) {
|
|
this._intersectionObserver.disconnect();
|
|
}
|
|
}
|
|
|
|
public play(): void {
|
|
if (this._currentItem?.can_play) {
|
|
this._runAction(this._currentItem);
|
|
}
|
|
}
|
|
|
|
protected render(): TemplateResult {
|
|
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>`;
|
|
}
|
|
|
|
const currentItem = this._currentItem;
|
|
|
|
const subtitle = this.hass.localize(
|
|
`ui.components.media-browser.class.${currentItem.media_class}`
|
|
);
|
|
|
|
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
|
|
const childrenMediaClass =
|
|
MediaClassBrowserSettings[currentItem.children_media_class];
|
|
|
|
return html`
|
|
${
|
|
currentItem.can_play
|
|
? html` <div
|
|
class="header ${classMap({
|
|
"no-img": !currentItem.thumbnail,
|
|
"no-dialog": !this.dialog,
|
|
})}"
|
|
@transitionend=${this._setHeaderHeight}
|
|
>
|
|
<div class="header-content">
|
|
${currentItem.thumbnail
|
|
? html`
|
|
<div
|
|
class="img"
|
|
style=${styleMap({
|
|
backgroundImage: currentItem.thumbnail
|
|
? `url(${currentItem.thumbnail})`
|
|
: "none",
|
|
})}
|
|
>
|
|
${this._narrow && currentItem?.can_play
|
|
? html`
|
|
<ha-fab
|
|
mini
|
|
.item=${currentItem}
|
|
@click=${this._actionClicked}
|
|
>
|
|
<ha-svg-icon
|
|
slot="icon"
|
|
.label=${this.hass.localize(
|
|
`ui.components.media-browser.${this.action}-media`
|
|
)}
|
|
.path=${this.action === "play"
|
|
? mdiPlay
|
|
: mdiPlus}
|
|
></ha-svg-icon>
|
|
${this.hass.localize(
|
|
`ui.components.media-browser.${this.action}`
|
|
)}
|
|
</ha-fab>
|
|
`
|
|
: ""}
|
|
</div>
|
|
`
|
|
: html``}
|
|
<div class="header-info">
|
|
<div class="breadcrumb">
|
|
<h1 class="title">${currentItem.title}</h1>
|
|
${subtitle
|
|
? html` <h2 class="subtitle">${subtitle}</h2> `
|
|
: ""}
|
|
</div>
|
|
${currentItem.can_play &&
|
|
(!currentItem.thumbnail || !this._narrow)
|
|
? html`
|
|
<mwc-button
|
|
raised
|
|
.item=${currentItem}
|
|
@click=${this._actionClicked}
|
|
>
|
|
<ha-svg-icon
|
|
.label=${this.hass.localize(
|
|
`ui.components.media-browser.${this.action}-media`
|
|
)}
|
|
.path=${this.action === "play"
|
|
? mdiPlay
|
|
: mdiPlus}
|
|
></ha-svg-icon>
|
|
${this.hass.localize(
|
|
`ui.components.media-browser.${this.action}`
|
|
)}
|
|
</mwc-button>
|
|
`
|
|
: ""}
|
|
</div>
|
|
</div>
|
|
</div>`
|
|
: ""
|
|
}
|
|
<div
|
|
class="content"
|
|
@scroll=${this._scroll}
|
|
@touchmove=${this._scroll}
|
|
>
|
|
${
|
|
this._error
|
|
? html`
|
|
<div class="container">
|
|
${this._renderError(this._error)}
|
|
</div>
|
|
`
|
|
: currentItem.children?.length
|
|
? childrenMediaClass.layout === "grid"
|
|
? html`
|
|
<div
|
|
class="children ${classMap({
|
|
portrait:
|
|
childrenMediaClass.thumbnail_ratio === "portrait",
|
|
})}"
|
|
>
|
|
${currentItem.children.map(
|
|
(child) => html`
|
|
<div
|
|
class="child"
|
|
.item=${child}
|
|
@click=${this._childClicked}
|
|
>
|
|
<ha-card outlined>
|
|
<div class="thumbnail">
|
|
${child.thumbnail
|
|
? html`
|
|
<div
|
|
class="${[
|
|
"app",
|
|
"directory",
|
|
].includes(child.media_class)
|
|
? "centered-image"
|
|
: ""} image lazythumbnail"
|
|
data-src=${child.thumbnail}
|
|
></div>
|
|
`
|
|
: html`
|
|
<div class="icon-holder image">
|
|
<ha-svg-icon
|
|
class="folder"
|
|
.path=${MediaClassBrowserSettings[
|
|
child.media_class === "directory"
|
|
? child.children_media_class ||
|
|
child.media_class
|
|
: child.media_class
|
|
].icon}
|
|
></ha-svg-icon>
|
|
</div>
|
|
`}
|
|
${child.can_play
|
|
? html`
|
|
<ha-icon-button
|
|
class="play ${classMap({
|
|
can_expand: child.can_expand,
|
|
})}"
|
|
.item=${child}
|
|
.label=${this.hass.localize(
|
|
`ui.components.media-browser.${this.action}-media`
|
|
)}
|
|
.path=${this.action === "play"
|
|
? mdiPlay
|
|
: mdiPlus}
|
|
@click=${this._actionClicked}
|
|
></ha-icon-button>
|
|
`
|
|
: ""}
|
|
</div>
|
|
<div class="title">
|
|
${child.title}
|
|
<paper-tooltip
|
|
fitToVisibleBounds
|
|
position="top"
|
|
offset="4"
|
|
>${child.title}</paper-tooltip
|
|
>
|
|
</div>
|
|
</ha-card>
|
|
</div>
|
|
`
|
|
)}
|
|
</div>
|
|
`
|
|
: html`
|
|
<mwc-list>
|
|
${currentItem.children.map(
|
|
(child) => html`
|
|
<mwc-list-item
|
|
@click=${this._childClicked}
|
|
.item=${child}
|
|
.graphic=${mediaClass.show_list_images
|
|
? "medium"
|
|
: "avatar"}
|
|
dir=${computeRTLDirection(this.hass)}
|
|
>
|
|
<div
|
|
class=${classMap({
|
|
graphic: true,
|
|
lazythumbnail:
|
|
mediaClass.show_list_images === true,
|
|
})}
|
|
data-src=${ifDefined(
|
|
mediaClass.show_list_images && child.thumbnail
|
|
? child.thumbnail
|
|
: undefined
|
|
)}
|
|
slot="graphic"
|
|
>
|
|
<ha-icon-button
|
|
class="play ${classMap({
|
|
show:
|
|
!mediaClass.show_list_images ||
|
|
!child.thumbnail,
|
|
})}"
|
|
.item=${child}
|
|
.label=${this.hass.localize(
|
|
`ui.components.media-browser.${this.action}-media`
|
|
)}
|
|
.path=${this.action === "play"
|
|
? mdiPlay
|
|
: mdiPlus}
|
|
@click=${this._actionClicked}
|
|
></ha-icon-button>
|
|
</div>
|
|
<span class="title">${child.title}</span>
|
|
</mwc-list-item>
|
|
<li divider role="separator"></li>
|
|
`
|
|
)}
|
|
</mwc-list>
|
|
`
|
|
: html`
|
|
<div class="container no-items">
|
|
${this.hass.localize(
|
|
"ui.components.media-browser.no_items"
|
|
)}
|
|
<br />
|
|
${currentItem.media_content_id ===
|
|
"media-source://media_source/local/."
|
|
? html`<br />${this.hass.localize(
|
|
"ui.components.media-browser.learn_adding_local_media",
|
|
"documentation",
|
|
html`<a
|
|
href=${documentationUrl(
|
|
this.hass,
|
|
"/more-info/local-media/add-media"
|
|
)}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>${this.hass.localize(
|
|
"ui.components.media-browser.documentation"
|
|
)}</a
|
|
>`
|
|
)}
|
|
<br />
|
|
${this.hass.localize(
|
|
"ui.components.media-browser.local_media_files"
|
|
)}`
|
|
: ""}
|
|
</div>
|
|
`
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
protected firstUpdated(): void {
|
|
this._measureCard();
|
|
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;
|
|
}
|
|
|
|
this._setError(undefined);
|
|
|
|
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 (!changedProps.has("entityId")) {
|
|
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;
|
|
fireEvent(this, "media-browsed", {
|
|
ids: this.navigateIds,
|
|
current: 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("_scrolled")) {
|
|
this._animateHeaderHeight();
|
|
} else if (changedProps.has("_currentItem")) {
|
|
this._setHeaderHeight();
|
|
this._attachIntersectionObserver();
|
|
}
|
|
}
|
|
|
|
private _actionClicked(ev: MouseEvent): void {
|
|
ev.stopPropagation();
|
|
const item = (ev.currentTarget as any).item;
|
|
|
|
this._runAction(item);
|
|
}
|
|
|
|
private _runAction(item: MediaPlayerItem): void {
|
|
fireEvent(this, "media-picked", { item });
|
|
}
|
|
|
|
private async _childClicked(ev: MouseEvent): Promise<void> {
|
|
const target = ev.currentTarget as any;
|
|
const item: MediaPlayerItem = target.item;
|
|
|
|
if (!item) {
|
|
return;
|
|
}
|
|
|
|
if (!item.can_expand) {
|
|
this._runAction(item);
|
|
return;
|
|
}
|
|
|
|
fireEvent(this, "media-browsed", {
|
|
ids: [...this.navigateIds, item],
|
|
});
|
|
}
|
|
|
|
private async _fetchData(
|
|
entityId: string,
|
|
mediaContentId?: string,
|
|
mediaContentType?: string
|
|
): Promise<MediaPlayerItem> {
|
|
return entityId !== BROWSER_PLAYER
|
|
? browseMediaPlayer(this.hass, entityId, mediaContentId, mediaContentType)
|
|
: browseLocalMediaPlayer(this.hass, mediaContentId);
|
|
}
|
|
|
|
private _measureCard(): void {
|
|
this._narrow = (this.dialog ? window.innerWidth : this.offsetWidth) < 450;
|
|
}
|
|
|
|
private async _attachResizeObserver(): Promise<void> {
|
|
if (!this._resizeObserver) {
|
|
await installResizeObserver();
|
|
this._resizeObserver = new ResizeObserver(
|
|
debounce(() => this._measureCard(), 250, false)
|
|
);
|
|
}
|
|
|
|
this._resizeObserver.observe(this);
|
|
}
|
|
|
|
/**
|
|
* Load thumbnails for images on demand as they become visible.
|
|
*/
|
|
private async _attachIntersectionObserver(): Promise<void> {
|
|
if (!("IntersectionObserver" in window) || !this._thumbnails) {
|
|
return;
|
|
}
|
|
if (!this._intersectionObserver) {
|
|
this._intersectionObserver = new IntersectionObserver(
|
|
async (entries, observer) => {
|
|
await Promise.all(
|
|
entries.map(async (entry) => {
|
|
if (!entry.isIntersecting) {
|
|
return;
|
|
}
|
|
const thumbnailCard = entry.target as HTMLElement;
|
|
let thumbnailUrl = thumbnailCard.dataset.src;
|
|
if (!thumbnailUrl) {
|
|
return;
|
|
}
|
|
if (thumbnailUrl.startsWith("/")) {
|
|
// Thumbnails served by local API require authentication
|
|
const signedPath = await getSignedPath(this.hass, thumbnailUrl);
|
|
thumbnailUrl = signedPath.path;
|
|
}
|
|
thumbnailCard.style.backgroundImage = `url(${thumbnailUrl})`;
|
|
observer.unobserve(thumbnailCard); // loaded, so no need to observe anymore
|
|
})
|
|
);
|
|
}
|
|
);
|
|
}
|
|
const observer = this._intersectionObserver!;
|
|
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`
|
|
<h2>
|
|
${this.hass.localize(
|
|
"ui.components.media-browser.no_local_media_found"
|
|
)}
|
|
</h2>
|
|
<p>
|
|
${this.hass.localize("ui.components.media-browser.no_media_folder")}
|
|
<br />
|
|
${this.hass.localize(
|
|
"ui.components.media-browser.setup_local_help",
|
|
"documentation",
|
|
html`<a
|
|
href=${documentationUrl(
|
|
this.hass,
|
|
"/more-info/local-media/setup-media"
|
|
)}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>${this.hass.localize(
|
|
"ui.components.media-browser.documentation"
|
|
)}</a
|
|
>`
|
|
)}
|
|
<br />
|
|
${this.hass.localize("ui.components.media-browser.local_media_files")}
|
|
</p>
|
|
`;
|
|
}
|
|
return html`<span class="error">${err.message}</span>`;
|
|
}
|
|
|
|
private async _setHeaderHeight() {
|
|
await this.updateComplete;
|
|
const header = this._header;
|
|
const content = this._content;
|
|
if (!header || !content) {
|
|
return;
|
|
}
|
|
this._headerOffsetHeight = header.offsetHeight;
|
|
content.style.marginTop = `${this._headerOffsetHeight}px`;
|
|
content.style.maxHeight = `calc(var(--media-browser-max-height, 100%) - ${this._headerOffsetHeight}px)`;
|
|
}
|
|
|
|
private _animateHeaderHeight() {
|
|
let start;
|
|
const animate = (time) => {
|
|
if (start === undefined) {
|
|
start = time;
|
|
}
|
|
const elapsed = time - start;
|
|
this._setHeaderHeight();
|
|
if (elapsed < 400) {
|
|
requestAnimationFrame(animate);
|
|
}
|
|
};
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
@eventOptions({ passive: true })
|
|
private _scroll(ev: Event): void {
|
|
const content = ev.currentTarget as HTMLDivElement;
|
|
if (!this._scrolled && content.scrollTop > this._headerOffsetHeight) {
|
|
this._scrolled = true;
|
|
} else if (this._scrolled && content.scrollTop < this._headerOffsetHeight) {
|
|
this._scrolled = false;
|
|
}
|
|
}
|
|
|
|
static get styles(): CSSResultGroup {
|
|
return [
|
|
haStyle,
|
|
css`
|
|
:host {
|
|
display: flex;
|
|
flex-direction: column;
|
|
position: relative;
|
|
}
|
|
|
|
ha-circular-progress {
|
|
--mdc-theme-primary: var(--primary-color);
|
|
display: flex;
|
|
justify-content: center;
|
|
margin: 40px;
|
|
}
|
|
|
|
.container {
|
|
padding: 16px;
|
|
}
|
|
|
|
.no-items {
|
|
padding-left: 32px;
|
|
}
|
|
|
|
.content {
|
|
overflow-y: auto;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* HEADER */
|
|
|
|
.header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
border-bottom: 1px solid var(--divider-color);
|
|
background-color: var(--card-background-color);
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
left: 0;
|
|
z-index: 5;
|
|
padding: 16px;
|
|
}
|
|
.header_button {
|
|
position: relative;
|
|
right: -8px;
|
|
}
|
|
.header-content {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
flex-grow: 1;
|
|
align-items: flex-start;
|
|
}
|
|
.header-content .img {
|
|
height: 175px;
|
|
width: 175px;
|
|
margin-right: 16px;
|
|
background-size: cover;
|
|
border-radius: 2px;
|
|
transition: width 0.4s, height 0.4s;
|
|
}
|
|
.header-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
align-self: stretch;
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
.header-info mwc-button {
|
|
display: block;
|
|
--mdc-theme-primary: var(--primary-color);
|
|
padding-bottom: 16px;
|
|
}
|
|
.breadcrumb {
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
flex-grow: 1;
|
|
padding-top: 16px;
|
|
}
|
|
.breadcrumb .title {
|
|
font-size: 32px;
|
|
line-height: 1.2;
|
|
font-weight: bold;
|
|
margin: 0;
|
|
overflow: hidden;
|
|
display: -webkit-box;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-line-clamp: 2;
|
|
padding-right: 8px;
|
|
}
|
|
.breadcrumb .previous-title {
|
|
font-size: 14px;
|
|
padding-bottom: 8px;
|
|
color: var(--secondary-text-color);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
cursor: pointer;
|
|
--mdc-icon-size: 14px;
|
|
}
|
|
.breadcrumb .subtitle {
|
|
font-size: 16px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
margin-bottom: 0;
|
|
transition: height 0.5s, margin 0.5s;
|
|
}
|
|
|
|
/* ============= CHILDREN ============= */
|
|
|
|
mwc-list {
|
|
--mdc-list-vertical-padding: 0;
|
|
--mdc-list-item-graphic-margin: 0;
|
|
--mdc-theme-text-icon-on-background: var(--secondary-text-color);
|
|
margin-top: 10px;
|
|
}
|
|
|
|
mwc-list li:last-child {
|
|
display: none;
|
|
}
|
|
|
|
mwc-list li[divider] {
|
|
border-bottom-color: var(--divider-color);
|
|
}
|
|
|
|
.children {
|
|
display: grid;
|
|
grid-template-columns: repeat(
|
|
auto-fit,
|
|
minmax(var(--media-browse-item-size, 175px), 0.1fr)
|
|
);
|
|
grid-gap: 16px;
|
|
padding: 16px;
|
|
}
|
|
|
|
:host([dialog]) .children {
|
|
grid-template-columns: repeat(
|
|
auto-fit,
|
|
minmax(var(--media-browse-item-size, 175px), 0.33fr)
|
|
);
|
|
}
|
|
|
|
.child {
|
|
display: flex;
|
|
flex-direction: column;
|
|
cursor: pointer;
|
|
}
|
|
|
|
ha-card {
|
|
position: relative;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.children ha-card .thumbnail {
|
|
width: 100%;
|
|
position: relative;
|
|
box-sizing: border-box;
|
|
transition: padding-bottom 0.1s ease-out;
|
|
padding-bottom: 100%;
|
|
}
|
|
|
|
.portrait.children ha-card .thumbnail {
|
|
padding-bottom: 150%;
|
|
}
|
|
|
|
ha-card .image {
|
|
border-radius: 3px 3px 0 0;
|
|
}
|
|
|
|
.image {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
background-size: cover;
|
|
background-repeat: no-repeat;
|
|
background-position: center;
|
|
}
|
|
|
|
.centered-image {
|
|
margin: 0 8px;
|
|
background-size: contain;
|
|
}
|
|
|
|
.children ha-card .icon-holder {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.child .folder {
|
|
color: var(--secondary-text-color);
|
|
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
|
}
|
|
|
|
.child .play {
|
|
position: absolute;
|
|
transition: color 0.5s;
|
|
border-radius: 50%;
|
|
top: calc(50% - 50px);
|
|
right: calc(50% - 35px);
|
|
opacity: 0;
|
|
transition: opacity 0.1s ease-out;
|
|
}
|
|
|
|
.child .play:not(.can_expand) {
|
|
--mdc-icon-button-size: 70px;
|
|
--mdc-icon-size: 48px;
|
|
}
|
|
|
|
ha-card:hover .play {
|
|
opacity: 1;
|
|
}
|
|
|
|
ha-card:hover .play:not(.can_expand) {
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
ha-card:hover .play.can_expand {
|
|
bottom: 8px;
|
|
}
|
|
|
|
.child .play.can_expand {
|
|
background-color: rgba(var(--rgb-card-background-color), 0.5);
|
|
top: auto;
|
|
bottom: 0px;
|
|
right: 8px;
|
|
transition: bottom 0.1s ease-out, opacity 0.1s ease-out;
|
|
}
|
|
|
|
.child .play:hover {
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
ha-card:hover .lazythumbnail {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.child .title {
|
|
font-size: 16px;
|
|
padding-top: 16px;
|
|
padding-left: 2px;
|
|
overflow: hidden;
|
|
display: -webkit-box;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-line-clamp: 1;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.child ha-card .title {
|
|
margin-bottom: 16px;
|
|
padding-left: 16px;
|
|
}
|
|
|
|
mwc-list-item .graphic {
|
|
background-size: contain;
|
|
border-radius: 2px;
|
|
display: flex;
|
|
align-content: center;
|
|
align-items: center;
|
|
line-height: initial;
|
|
}
|
|
|
|
mwc-list-item .graphic .play {
|
|
opacity: 0;
|
|
transition: all 0.5s;
|
|
background-color: rgba(var(--rgb-card-background-color), 0.5);
|
|
border-radius: 50%;
|
|
--mdc-icon-button-size: 40px;
|
|
}
|
|
|
|
mwc-list-item:hover .graphic .play {
|
|
opacity: 1;
|
|
color: var(--primary-text-color);
|
|
}
|
|
|
|
mwc-list-item .graphic .play.show {
|
|
opacity: 1;
|
|
background-color: transparent;
|
|
}
|
|
|
|
mwc-list-item .title {
|
|
margin-left: 16px;
|
|
}
|
|
mwc-list-item[dir="rtl"] .title {
|
|
margin-right: 16px;
|
|
margin-left: 0;
|
|
}
|
|
|
|
/* ============= Narrow ============= */
|
|
|
|
:host([narrow]) {
|
|
padding: 0;
|
|
}
|
|
|
|
:host([narrow]) .media-source {
|
|
padding: 0 24px;
|
|
}
|
|
|
|
:host([narrow]) .children {
|
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
|
|
}
|
|
|
|
:host([narrow]) .breadcrumb .title {
|
|
font-size: 24px;
|
|
}
|
|
:host([narrow]) .header {
|
|
padding: 0;
|
|
}
|
|
:host([narrow]) .header.no-dialog {
|
|
display: block;
|
|
}
|
|
:host([narrow]) .header_button {
|
|
position: absolute;
|
|
top: 14px;
|
|
right: 8px;
|
|
}
|
|
:host([narrow]) .header-content {
|
|
flex-direction: column;
|
|
flex-wrap: nowrap;
|
|
}
|
|
:host([narrow]) .header-content .img {
|
|
height: auto;
|
|
width: 100%;
|
|
margin-right: 0;
|
|
padding-bottom: 50%;
|
|
margin-bottom: 8px;
|
|
position: relative;
|
|
background-position: center;
|
|
border-radius: 0;
|
|
transition: width 0.4s, height 0.4s, padding-bottom 0.4s;
|
|
}
|
|
ha-fab {
|
|
position: absolute;
|
|
--mdc-theme-secondary: var(--primary-color);
|
|
bottom: -20px;
|
|
right: 20px;
|
|
}
|
|
:host([narrow]) .header-info mwc-button {
|
|
margin-top: 16px;
|
|
margin-bottom: 8px;
|
|
}
|
|
:host([narrow]) .header-info {
|
|
padding: 0 16px 8px;
|
|
}
|
|
|
|
/* ============= Scroll ============= */
|
|
:host([scroll]) .breadcrumb .subtitle {
|
|
height: 0;
|
|
margin: 0;
|
|
}
|
|
:host([scroll]) .breadcrumb .title {
|
|
-webkit-line-clamp: 1;
|
|
}
|
|
:host(:not([narrow])[scroll]) .header:not(.no-img) ha-icon-button {
|
|
align-self: center;
|
|
}
|
|
:host([scroll]) .header-info mwc-button,
|
|
.no-img .header-info mwc-button {
|
|
padding-right: 4px;
|
|
}
|
|
:host([scroll][narrow]) .no-img .header-info mwc-button {
|
|
padding-right: 16px;
|
|
}
|
|
:host([scroll]) .header-info {
|
|
flex-direction: row;
|
|
}
|
|
:host([scroll]) .header-info mwc-button {
|
|
align-self: center;
|
|
margin-top: 0;
|
|
margin-bottom: 0;
|
|
padding-bottom: 0;
|
|
}
|
|
:host([scroll][narrow]) .no-img .header-info {
|
|
flex-direction: row-reverse;
|
|
}
|
|
:host([scroll][narrow]) .header-info {
|
|
padding: 20px 24px 10px 24px;
|
|
align-items: center;
|
|
}
|
|
:host([scroll]) .header-content {
|
|
align-items: flex-end;
|
|
flex-direction: row;
|
|
}
|
|
:host([scroll]) .header-content .img {
|
|
height: 75px;
|
|
width: 75px;
|
|
}
|
|
:host([scroll]) .breadcrumb {
|
|
padding-top: 0;
|
|
align-self: center;
|
|
}
|
|
:host([scroll][narrow]) .header-content .img {
|
|
height: 100px;
|
|
width: 100px;
|
|
padding-bottom: initial;
|
|
margin-bottom: 0;
|
|
}
|
|
:host([scroll]) ha-fab {
|
|
bottom: 0px;
|
|
right: -24px;
|
|
--mdc-fab-box-shadow: none;
|
|
--mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5);
|
|
}
|
|
`,
|
|
];
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"ha-media-player-browse": HaMediaPlayerBrowse;
|
|
}
|
|
}
|