Add a Grid/List toggle for Media Browser (#18256)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
karwosts 2023-11-28 08:34:34 -08:00 committed by GitHub
parent 5a9ccc5ae7
commit 9f1cd80a31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 204 additions and 39 deletions

View File

@ -1,11 +1,20 @@
import { mdiArrowLeft, mdiClose } from "@mdi/js"; import { ActionDetail } from "@material/mwc-list";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import {
mdiAlphaABoxOutline,
mdiArrowLeft,
mdiClose,
mdiDotsVertical,
mdiGrid,
mdiListBoxOutline,
} from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event"; import { HASSDomEvent, fireEvent } from "../../common/dom/fire_event";
import type { import type {
MediaPickedEvent, MediaPickedEvent,
MediaPlayerBrowseAction, MediaPlayerBrowseAction,
MediaPlayerItem, MediaPlayerItem,
MediaPlayerLayoutType,
} from "../../data/media-player"; } from "../../data/media-player";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
@ -18,6 +27,7 @@ import type {
MediaPlayerItemId, MediaPlayerItemId,
} from "./ha-media-player-browse"; } from "./ha-media-player-browse";
import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog"; import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog";
import { stopPropagation } from "../../common/dom/stop_propagation";
@customElement("dialog-media-player-browse") @customElement("dialog-media-player-browse")
class DialogMediaPlayerBrowse extends LitElement { class DialogMediaPlayerBrowse extends LitElement {
@ -29,6 +39,8 @@ class DialogMediaPlayerBrowse extends LitElement {
@state() private _params?: MediaPlayerBrowseDialogParams; @state() private _params?: MediaPlayerBrowseDialogParams;
@state() _preferredLayout: MediaPlayerLayoutType = "auto";
@query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse; @query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse;
public showDialog(params: MediaPlayerBrowseDialogParams): void { public showDialog(params: MediaPlayerBrowseDialogParams): void {
@ -45,6 +57,7 @@ class DialogMediaPlayerBrowse extends LitElement {
this._params = undefined; this._params = undefined;
this._navigateIds = undefined; this._navigateIds = undefined;
this._currentItem = undefined; this._currentItem = undefined;
this._preferredLayout = "auto";
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@ -84,13 +97,54 @@ class DialogMediaPlayerBrowse extends LitElement {
) )
: this._currentItem.title} : this._currentItem.title}
</span> </span>
<ha-media-manage-button <ha-media-manage-button
slot="actionItems" slot="actionItems"
.hass=${this.hass} .hass=${this.hass}
.currentItem=${this._currentItem} .currentItem=${this._currentItem}
@media-refresh=${this._refreshMedia} @media-refresh=${this._refreshMedia}
></ha-media-manage-button> ></ha-media-manage-button>
<ha-button-menu
slot="actionItems"
@action=${this._handleMenuAction}
@closed=${stopPropagation}
fixed
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item graphic="icon">
${this.hass.localize("ui.components.media-browser.auto")}
<ha-svg-icon
class=${this._preferredLayout === "auto"
? "selected_menu_item"
: ""}
slot="graphic"
.path=${mdiAlphaABoxOutline}
></ha-svg-icon>
</mwc-list-item>
<mwc-list-item graphic="icon">
${this.hass.localize("ui.components.media-browser.grid")}
<ha-svg-icon
class=${this._preferredLayout === "grid"
? "selected_menu_item"
: ""}
slot="graphic"
.path=${mdiGrid}
></ha-svg-icon>
</mwc-list-item>
<mwc-list-item graphic="icon">
${this.hass.localize("ui.components.media-browser.list")}
<ha-svg-icon
slot="graphic"
class=${this._preferredLayout === "list"
? "selected_menu_item"
: ""}
.path=${mdiListBoxOutline}
></ha-svg-icon>
</mwc-list-item>
</ha-button-menu>
<ha-icon-button <ha-icon-button
.label=${this.hass.localize("ui.dialogs.generic.close")} .label=${this.hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose} .path=${mdiClose}
@ -104,6 +158,7 @@ class DialogMediaPlayerBrowse extends LitElement {
.entityId=${this._params.entityId} .entityId=${this._params.entityId}
.navigateIds=${this._navigateIds} .navigateIds=${this._navigateIds}
.action=${this._action} .action=${this._action}
.preferredLayout=${this._preferredLayout}
@close-dialog=${this.closeDialog} @close-dialog=${this.closeDialog}
@media-picked=${this._mediaPicked} @media-picked=${this._mediaPicked}
@media-browsed=${this._mediaBrowsed} @media-browsed=${this._mediaBrowsed}
@ -112,6 +167,20 @@ class DialogMediaPlayerBrowse extends LitElement {
`; `;
} }
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._preferredLayout = "auto";
break;
case 1:
this._preferredLayout = "grid";
break;
case 2:
this._preferredLayout = "list";
break;
}
}
private _goBack() { private _goBack() {
this._navigateIds = this._navigateIds?.slice(0, -1); this._navigateIds = this._navigateIds?.slice(0, -1);
this._currentItem = undefined; this._currentItem = undefined;

View File

@ -35,6 +35,7 @@ import {
MediaClassBrowserSettings, MediaClassBrowserSettings,
MediaPickedEvent, MediaPickedEvent,
MediaPlayerBrowseAction, MediaPlayerBrowseAction,
MediaPlayerLayoutType,
} from "../../data/media-player"; } from "../../data/media-player";
import { browseLocalMediaPlayer } from "../../data/media_source"; import { browseLocalMediaPlayer } from "../../data/media_source";
import { isTTSMediaSource } from "../../data/tts"; import { isTTSMediaSource } from "../../data/tts";
@ -87,6 +88,8 @@ export class HaMediaPlayerBrowse extends LitElement {
@property() public action: MediaPlayerBrowseAction = "play"; @property() public action: MediaPlayerBrowseAction = "play";
@property() public preferredLayout: MediaPlayerLayoutType = "auto";
@property({ type: Boolean }) public dialog = false; @property({ type: Boolean }) public dialog = false;
@property() public navigateIds!: MediaPlayerItemId[]; @property() public navigateIds!: MediaPlayerItemId[];
@ -477,7 +480,9 @@ export class HaMediaPlayerBrowse extends LitElement {
)} )}
</div> </div>
` `
: childrenMediaClass.layout === "grid" : this.preferredLayout === "grid" ||
(this.preferredLayout === "auto" &&
childrenMediaClass.layout === "grid")
? html` ? html`
<lit-virtualizer <lit-virtualizer
scroller scroller
@ -569,11 +574,12 @@ export class HaMediaPlayerBrowse extends LitElement {
${child.thumbnail ${child.thumbnail
? html` ? html`
<div <div
class="${["app", "directory"].includes(child.media_class) class="${classMap({
? "centered-image" "centered-image": ["app", "directory"].includes(
: ""} ${isBrandUrl(child.thumbnail) child.media_class
? "brand-image" ),
: ""} image" "brand-image": isBrandUrl(child.thumbnail),
})} image"
style="background-image: ${until(backgroundImage, "")}" style="background-image: ${until(backgroundImage, "")}"
></div> ></div>
` `
@ -634,26 +640,37 @@ export class HaMediaPlayerBrowse extends LitElement {
.graphic=${mediaClass.show_list_images ? "medium" : "avatar"} .graphic=${mediaClass.show_list_images ? "medium" : "avatar"}
dir=${computeRTLDirection(this.hass)} dir=${computeRTLDirection(this.hass)}
> >
<div ${backgroundImage === "none" && !child.can_play
class=${classMap({ ? html`<ha-svg-icon
graphic: true, .path=${MediaClassBrowserSettings[
thumbnail: mediaClass.show_list_images === true, child.media_class === "directory"
})} ? child.children_media_class || child.media_class
style="background-image: ${until(backgroundImage, "")}" : child.media_class
slot="graphic" ].icon}
> slot="graphic"
<ha-icon-button ></ha-svg-icon>`
class="play ${classMap({ : html`<div
show: !mediaClass.show_list_images || !child.thumbnail, class=${classMap({
})}" graphic: true,
.item=${child} thumbnail: mediaClass.show_list_images === true,
.label=${this.hass.localize( })}
`ui.components.media-browser.${this.action}-media` style="background-image: ${until(backgroundImage, "")}"
)} slot="graphic"
.path=${this.action === "play" ? mdiPlay : mdiPlus} >
@click=${this._actionClicked} ${child.can_play
></ha-icon-button> ? html`<ha-icon-button
</div> 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>`
: nothing}
</div>`}
<span class="title">${child.title}</span> <span class="title">${child.title}</span>
</mwc-list-item> </mwc-list-item>
`; `;
@ -899,7 +916,6 @@ export class HaMediaPlayerBrowse extends LitElement {
overflow-y: auto; overflow-y: auto;
box-sizing: border-box; box-sizing: border-box;
height: 100%; height: 100%;
position: relative;
} }
/* HEADER */ /* HEADER */
@ -913,7 +929,7 @@ export class HaMediaPlayerBrowse extends LitElement {
top: 0; top: 0;
right: 0; right: 0;
left: 0; left: 0;
z-index: 5; z-index: 3;
padding: 16px; padding: 16px;
} }
.header_button { .header_button {
@ -1154,6 +1170,8 @@ export class HaMediaPlayerBrowse extends LitElement {
mwc-list-item .graphic { mwc-list-item .graphic {
background-size: contain; background-size: contain;
background-repeat: no-repeat;
background-position: center;
border-radius: 2px; border-radius: 2px;
display: flex; display: flex;
align-content: center; align-content: center;

View File

@ -106,6 +106,8 @@ export type MediaPlayerBrowseAction = "pick" | "play";
export const BROWSER_PLAYER = "browser"; export const BROWSER_PLAYER = "browser";
export type MediaPlayerLayoutType = "grid" | "list" | "auto";
export type MediaClassBrowserSetting = { export type MediaClassBrowserSetting = {
icon: string; icon: string;
thumbnail_ratio?: string; thumbnail_ratio?: string;
@ -117,12 +119,13 @@ export const MediaClassBrowserSettings: {
[type: string]: MediaClassBrowserSetting; [type: string]: MediaClassBrowserSetting;
} = { } = {
album: { icon: mdiAlbum, layout: "grid" }, album: { icon: mdiAlbum, layout: "grid" },
app: { icon: mdiApplication, layout: "grid" }, app: { icon: mdiApplication, layout: "grid", show_list_images: true },
artist: { icon: mdiAccountMusic, layout: "grid", show_list_images: true }, artist: { icon: mdiAccountMusic, layout: "grid", show_list_images: true },
channel: { channel: {
icon: mdiTelevisionClassic, icon: mdiTelevisionClassic,
thumbnail_ratio: "portrait", thumbnail_ratio: "portrait",
layout: "grid", layout: "grid",
show_list_images: true,
}, },
composer: { composer: {
icon: mdiAccountMusicOutline, icon: mdiAccountMusicOutline,
@ -139,6 +142,7 @@ export const MediaClassBrowserSettings: {
icon: mdiTelevisionClassic, icon: mdiTelevisionClassic,
layout: "grid", layout: "grid",
thumbnail_ratio: "portrait", thumbnail_ratio: "portrait",
show_list_images: true,
}, },
game: { game: {
icon: mdiGamepadVariant, icon: mdiGamepadVariant,
@ -146,15 +150,21 @@ export const MediaClassBrowserSettings: {
thumbnail_ratio: "portrait", thumbnail_ratio: "portrait",
}, },
genre: { icon: mdiDramaMasks, layout: "grid", show_list_images: true }, genre: { icon: mdiDramaMasks, layout: "grid", show_list_images: true },
image: { icon: mdiImage, layout: "grid" }, image: { icon: mdiImage, layout: "grid", show_list_images: true },
movie: { icon: mdiMovie, thumbnail_ratio: "portrait", layout: "grid" }, movie: {
music: { icon: mdiMusic }, icon: mdiMovie,
thumbnail_ratio: "portrait",
layout: "grid",
show_list_images: true,
},
music: { icon: mdiMusic, show_list_images: true },
playlist: { icon: mdiPlaylistMusic, layout: "grid", show_list_images: true }, playlist: { icon: mdiPlaylistMusic, layout: "grid", show_list_images: true },
podcast: { icon: mdiPodcast, layout: "grid" }, podcast: { icon: mdiPodcast, layout: "grid" },
season: { season: {
icon: mdiTelevisionClassic, icon: mdiTelevisionClassic,
layout: "grid", layout: "grid",
thumbnail_ratio: "portrait", thumbnail_ratio: "portrait",
show_list_images: true,
}, },
track: { icon: mdiFileMusic }, track: { icon: mdiFileMusic },
tv_show: { tv_show: {
@ -163,7 +173,7 @@ export const MediaClassBrowserSettings: {
thumbnail_ratio: "portrait", thumbnail_ratio: "portrait",
}, },
url: { icon: mdiWeb }, url: { icon: mdiWeb },
video: { icon: mdiVideo, layout: "grid" }, video: { icon: mdiVideo, layout: "grid", show_list_images: true },
}; };
export interface MediaPickedEvent { export interface MediaPickedEvent {

View File

@ -1,4 +1,11 @@
import { mdiArrowLeft } from "@mdi/js"; import {
mdiGrid,
mdiListBoxOutline,
mdiArrowLeft,
mdiAlphaABoxOutline,
mdiDotsVertical,
} from "@mdi/js";
import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-button"; import "@material/mwc-button";
import { import {
css, css,
@ -26,6 +33,7 @@ import {
MediaPickedEvent, MediaPickedEvent,
MediaPlayerItem, MediaPlayerItem,
mediaPlayerPlayMedia, mediaPlayerPlayMedia,
MediaPlayerLayoutType,
} from "../../data/media-player"; } from "../../data/media-player";
import { import {
ResolvedMediaSource, ResolvedMediaSource,
@ -64,6 +72,8 @@ class PanelMediaBrowser extends LitElement {
@state() _currentItem?: MediaPlayerItem; @state() _currentItem?: MediaPlayerItem;
@state() _preferredLayout: MediaPlayerLayoutType = "auto";
private _navigateIds: MediaPlayerItemId[] = [ private _navigateIds: MediaPlayerItemId[] = [
{ {
media_content_id: undefined, media_content_id: undefined,
@ -113,10 +123,48 @@ class PanelMediaBrowser extends LitElement {
.currentItem=${this._currentItem} .currentItem=${this._currentItem}
@media-refresh=${this._refreshMedia} @media-refresh=${this._refreshMedia}
></ha-media-manage-button> ></ha-media-manage-button>
<ha-button-menu slot="actionItems" @action=${this._handleMenuAction}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item graphic="icon">
${this.hass.localize("ui.components.media-browser.auto")}
<ha-svg-icon
class=${this._preferredLayout === "auto"
? "selected_menu_item"
: ""}
slot="graphic"
.path=${mdiAlphaABoxOutline}
></ha-svg-icon>
</mwc-list-item>
<mwc-list-item graphic="icon">
${this.hass.localize("ui.components.media-browser.grid")}
<ha-svg-icon
class=${this._preferredLayout === "grid"
? "selected_menu_item"
: ""}
slot="graphic"
.path=${mdiGrid}
></ha-svg-icon>
</mwc-list-item>
<mwc-list-item graphic="icon">
${this.hass.localize("ui.components.media-browser.list")}
<ha-svg-icon
slot="graphic"
class=${this._preferredLayout === "list"
? "selected_menu_item"
: ""}
.path=${mdiListBoxOutline}
></ha-svg-icon>
</mwc-list-item>
</ha-button-menu>
<ha-media-player-browse <ha-media-player-browse
.hass=${this.hass} .hass=${this.hass}
.entityId=${this._entityId} .entityId=${this._entityId}
.navigateIds=${this._navigateIds} .navigateIds=${this._navigateIds}
.preferredLayout=${this._preferredLayout}
@media-picked=${this._mediaPicked} @media-picked=${this._mediaPicked}
@media-browsed=${this._mediaBrowsed} @media-browsed=${this._mediaBrowsed}
></ha-media-player-browse> ></ha-media-player-browse>
@ -130,6 +178,20 @@ class PanelMediaBrowser extends LitElement {
`; `;
} }
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._preferredLayout = "auto";
break;
case 1:
this._preferredLayout = "grid";
break;
case 2:
this._preferredLayout = "list";
break;
}
}
public willUpdate(changedProps: PropertyValues): void { public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps); super.willUpdate(changedProps);
@ -288,6 +350,9 @@ class PanelMediaBrowser extends LitElement {
:host([narrow]) ha-media-player-browse { :host([narrow]) ha-media-player-browse {
height: calc(100vh - (57px + var(--header-height))); height: calc(100vh - (57px + var(--header-height)));
} }
.selected_menu_item {
color: var(--primary-color);
}
ha-bar-media-player { ha-bar-media-player {
position: fixed; position: fixed;

View File

@ -703,7 +703,10 @@
"url": "URL", "url": "URL",
"video": "Video" "video": "Video"
}, },
"media_player_unavailable": "The selected media player is unavailable." "media_player_unavailable": "The selected media player is unavailable.",
"auto": "Auto",
"grid": "Grid",
"list": "List"
}, },
"calendar": { "calendar": {
"label": "Calendar", "label": "Calendar",